혼행족이 선호하는 여행지 분석

  • 분석자 : 이재성
  • 분석기간 : 2020년 8월 ~ 9월

1. 프로젝트 개요

몇년 전부터 혼밥, 혼술, 혼행 등 혼자 하는 콘텐츠가 많아지고 있는 추세입니다. 이 중, 혼행(혼자 여행)에 초점을 맞춰 혼행족들이 선호하는 여행지를 살펴보고 그 여행지가 가지는 특징을 살펴보고자 합니다. 유사도 분석을 바탕으로 혼행이 아니더라도 여행을 계획하는 사람들에게 여행지와 관련된 인기 장소, 먹거리 등 지역 관련된 다양한 키워드를 제시하고자 합니다. 마지막으로 코사인 유사도를 활용한 여행지 추천시스템을 구현하여 선호하는 키워드를 입력하면 특정 여행지를 추천해주는 기능도 구현하고자 합니다.

1.1 데이터 수집

  • 데이터 : 네이버 블로그 본문 1000개 (네이버 API를 통한 크롤링)
  • 검색어 : '국내여행 +혼자 -가족,친구'
  • 수집일 : 2020년 7월 15일
  • 검색방식 : 정확도 기반 검색

1.2 개발 환경

pandas == 1.0.5

numpy == 1.18.5

matplotlib == 3.2.2

seaborn == 0.10.1

eunjeon == 0.4.0

re == 2.2.1

sklearn == 0.23.1

networkx == 2.4

community == 0.14

gensim == 3.8.3

1.3 라이브러리

  • 현재 항목에서는 기본적인 라이브러리만을 정리했습니다.
In [1]:
import pandas as pd
import numpy as np
from collections import Counter # 갯수 파악 라이브러리
from eunjeon import Mecab       # 자연어 처리 라이브러리
import matplotlib.pyplot as plt
import seaborn as sns
import re

plt.rc('font', family='Malgun Gothic')
pd.options.display.max_columns = 50  # 컬럼 출력 개수 설정
%matplotlib inline

1.4 데이터 로드

In [2]:
data = pd.read_csv('travel_crawl.csv')

print(data.shape)
data.head()
(1000, 7)
Out[2]:
Title Link Description Blogger Name Blogger Link Post Date Post Contents
0 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! https://blog.naver.com/love151419?Redirect=Log... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! 안녕하세요호! 현짱입... 현짱이네 https://blog.naver.com/love151419 20190705 \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비...
1 여자 혼자 1박2일 군산 국내여행 part.2 https://blog.naver.com/kansk92?Redirect=Log&lo... 예전부터 군산 너무 가보고 싶었는데 5년만에 혼자 다녀온 국내여행도 대성공이었다 뚜... ma vie en rose. https://blog.naver.com/kansk92 20181227 \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군...
2 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... https://blog.naver.com/okinawa100?Redirect=Log... 저만 혼자 들어가서 아주 예수님 코스프레 하고 나왔습니다!! 애들이 저만 바라봐서 ... 시유 :) https://blog.naver.com/okinawa100 20200712 \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑...
3 국내 여행용캐리어 추천, 확장되는 20인치 하드캐리어 키코... https://blog.naver.com/accentv?Redirect=Log&lo... 국내여행으로 좋고, 해외도 혼자 출장을 갈때 사용하면 좋은 부담없는 사이즈~ 캐리어... 작지만 확실한 행복 :D https://blog.naver.com/accentv 20200709 \n\n\n\n\n\n\n원더풀코리아♥\n\n\n\n\n국내 여행용캐리어 추천, ...
4 [국내여행]원산도&태안 허슬러 차박 그리고 낚시 https://blog.naver.com/rkaantm?Redirect=Log&lo... 10일 캠핑&낚시 이용권 암튼 저는 여름에 프랑스를 가지 못하니, 국내 여행... 완두콩의 행복한 여행 그리고 사진 https://blog.naver.com/rkaantm 20200617 \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박...
In [3]:
# 컬럼 이름 정리(언더바 삽입)
data.columns = ['Title', 'Link', 'Description', 'Blogger_Name', 'Blogger_Link', 'Post_Date', 'Post_Contents']

data.head()
Out[3]:
Title Link Description Blogger_Name Blogger_Link Post_Date Post_Contents
0 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! https://blog.naver.com/love151419?Redirect=Log... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! 안녕하세요호! 현짱입... 현짱이네 https://blog.naver.com/love151419 20190705 \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비...
1 여자 혼자 1박2일 군산 국내여행 part.2 https://blog.naver.com/kansk92?Redirect=Log&lo... 예전부터 군산 너무 가보고 싶었는데 5년만에 혼자 다녀온 국내여행도 대성공이었다 뚜... ma vie en rose. https://blog.naver.com/kansk92 20181227 \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군...
2 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... https://blog.naver.com/okinawa100?Redirect=Log... 저만 혼자 들어가서 아주 예수님 코스프레 하고 나왔습니다!! 애들이 저만 바라봐서 ... 시유 :) https://blog.naver.com/okinawa100 20200712 \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑...
3 국내 여행용캐리어 추천, 확장되는 20인치 하드캐리어 키코... https://blog.naver.com/accentv?Redirect=Log&lo... 국내여행으로 좋고, 해외도 혼자 출장을 갈때 사용하면 좋은 부담없는 사이즈~ 캐리어... 작지만 확실한 행복 :D https://blog.naver.com/accentv 20200709 \n\n\n\n\n\n\n원더풀코리아♥\n\n\n\n\n국내 여행용캐리어 추천, ...
4 [국내여행]원산도&태안 허슬러 차박 그리고 낚시 https://blog.naver.com/rkaantm?Redirect=Log&lo... 10일 캠핑&낚시 이용권 암튼 저는 여름에 프랑스를 가지 못하니, 국내 여행... 완두콩의 행복한 여행 그리고 사진 https://blog.naver.com/rkaantm 20200617 \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박...
In [4]:
# 네트워크 오류 등으로 발생한 중복 입력 값을 제거하겠습니다.

print('원본 포스팅 글 수 :',data.shape)

data = data.drop_duplicates(['Post_Contents'], keep = 'last') # 가장 최근 내용을 남기겠습니다.

print('중복 제거 후 포스팅 글 수 :', data.shape)
data.head()
원본 포스팅 글 수 : (1000, 7)
중복 제거 후 포스팅 글 수 : (982, 7)
Out[4]:
Title Link Description Blogger_Name Blogger_Link Post_Date Post_Contents
0 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! https://blog.naver.com/love151419?Redirect=Log... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! 안녕하세요호! 현짱입... 현짱이네 https://blog.naver.com/love151419 20190705 \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비...
1 여자 혼자 1박2일 군산 국내여행 part.2 https://blog.naver.com/kansk92?Redirect=Log&lo... 예전부터 군산 너무 가보고 싶었는데 5년만에 혼자 다녀온 국내여행도 대성공이었다 뚜... ma vie en rose. https://blog.naver.com/kansk92 20181227 \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군...
2 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... https://blog.naver.com/okinawa100?Redirect=Log... 저만 혼자 들어가서 아주 예수님 코스프레 하고 나왔습니다!! 애들이 저만 바라봐서 ... 시유 :) https://blog.naver.com/okinawa100 20200712 \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑...
3 국내 여행용캐리어 추천, 확장되는 20인치 하드캐리어 키코... https://blog.naver.com/accentv?Redirect=Log&lo... 국내여행으로 좋고, 해외도 혼자 출장을 갈때 사용하면 좋은 부담없는 사이즈~ 캐리어... 작지만 확실한 행복 :D https://blog.naver.com/accentv 20200709 \n\n\n\n\n\n\n원더풀코리아♥\n\n\n\n\n국내 여행용캐리어 추천, ...
4 [국내여행]원산도&태안 허슬러 차박 그리고 낚시 https://blog.naver.com/rkaantm?Redirect=Log&lo... 10일 캠핑&낚시 이용권 암튼 저는 여름에 프랑스를 가지 못하니, 국내 여행... 완두콩의 행복한 여행 그리고 사진 https://blog.naver.com/rkaantm 20200617 \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박...

중복된 포스팅 글 18개를 제거한 총 982개의 포스팅 글을 분석에 활용하겠습니다.

In [5]:
# 원본은 따로 보관하겠습니다.

data['raw_Post_Contents'] = data['Post_Contents']

2. 전처리

2.1 소문자 변환

In [6]:
# 파이썬에서는 대문자와 소문자를 따로 처리하므로 Post_Contents에 포함된 영어는 모두 소문자로 변환하겠습니다.
data['Post_Contents'] = data['Post_Contents'].str.lower()
In [7]:
data.head()
Out[7]:
Title Link Description Blogger_Name Blogger_Link Post_Date Post_Contents raw_Post_Contents
0 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! https://blog.naver.com/love151419?Redirect=Log... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! 안녕하세요호! 현짱입... 현짱이네 https://blog.naver.com/love151419 20190705 \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비... \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비...
1 여자 혼자 1박2일 군산 국내여행 part.2 https://blog.naver.com/kansk92?Redirect=Log&lo... 예전부터 군산 너무 가보고 싶었는데 5년만에 혼자 다녀온 국내여행도 대성공이었다 뚜... ma vie en rose. https://blog.naver.com/kansk92 20181227 \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군... \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군...
2 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... https://blog.naver.com/okinawa100?Redirect=Log... 저만 혼자 들어가서 아주 예수님 코스프레 하고 나왔습니다!! 애들이 저만 바라봐서 ... 시유 :) https://blog.naver.com/okinawa100 20200712 \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑... \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑...
3 국내 여행용캐리어 추천, 확장되는 20인치 하드캐리어 키코... https://blog.naver.com/accentv?Redirect=Log&lo... 국내여행으로 좋고, 해외도 혼자 출장을 갈때 사용하면 좋은 부담없는 사이즈~ 캐리어... 작지만 확실한 행복 :D https://blog.naver.com/accentv 20200709 \n\n\n\n\n\n\n원더풀코리아♥\n\n\n\n\n국내 여행용캐리어 추천, ... \n\n\n\n\n\n\n원더풀코리아♥\n\n\n\n\n국내 여행용캐리어 추천, ...
4 [국내여행]원산도&태안 허슬러 차박 그리고 낚시 https://blog.naver.com/rkaantm?Redirect=Log&lo... 10일 캠핑&낚시 이용권 암튼 저는 여름에 프랑스를 가지 못하니, 국내 여행... 완두콩의 행복한 여행 그리고 사진 https://blog.naver.com/rkaantm 20200617 \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박... \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박...

2.2 개행문자 제거

In [8]:
# 개행문자를 제거하여 contents 라는 리스트에 담겠습니다.

contents = []
posts = data['Post_Contents']
for post in posts:
    post = str(post).replace('\n','').replace('\u200b','').replace('\xa0','').replace('\t','')
    contents.append(str(post))

contents[1]
Out[8]:
'daily life여자 혼자 1박2일 군산 국내여행 part.2 칸쵸 ・ 2018. 12. 27. 16:16url 복사 이웃추가본문 기타 기능번역보기 ㅤㅤ여자 혼자 1박2일 군산 국내여행 part.2     숙소에서 잠시 쉬고 다시 나와,@군산근대역사박물관 으로! 박물관+건축관+미술관+비응함 입장가능한통합권은 박물관 입장권이랑 1000원 차이난다(통합권 3000원 / 박물관 입장권 2000원)하지만 나는 박물관만 관람!!!!   3층 근대생활관이렇게 옛날 교복이나 옷 같은거피팅해볼수 있어서 모자라도 한번 써봄 ㅋ이 비슷한거 나도 있긴 한데...   난 어릴때부터 아기고무신을 완!!!전귀여워했다..물론 지금도..근데 여기 있어서 또 심쿵,..ㅇ<-<   저기 또 있어 엉엉🥺   아니 또ㅠㅠㅠㅠㅠ너무 좋아서 신발 사진만 3장 남겼나보다..   아픈 역사의 순간들이지만확실히 3층 근대생활관이 사람들의 관심이제일 컸던거 같다! 나역시!   3층은 1930년대의 모습을 재현해둔곳이라 모든게 다 신기했다! 이리=익산이래용💺   @군산 부잔교(뜬다리부두)박물관 바로 옆이라부잔교+진포해양테마공원 구경할겸 왔다!저 다리 한번 건너가보고싶었는데일반인은 가지말라고해서 말 잘 듣는 일반인 1인,그냥 여기서 사진만 찰칵 ㅋㅋㅋ이 다리들도 일제강점기 시절,일본에 쌀을 보내기 위해 만들었단다          물이 빠진 상태라 신기했다 마음은 선유도에 가고싶었지만 넘 멀어서이렇게나마 군산 바다 구경!!!!바다있는 지역에서 나고 자랐지만넘 좋았다 속이 뻥ㅇ~~~👍🏻💙               @군산 진포해양테마공원뭐 일부러 보러갈정도는 아니지만나는 나름 흥미로웠다 ㅋㅋㅋ         두번째 사진에서 보이는 군산196 카페에많이들 가지만 나는 근처 건물 외관들 구경만!   (아마도)비행기 두대 ✈️ ☁️ ✈️  안녕 나야   원래 태극기 전체가 있는걸로 아는데왜 윗부분이 없는걸까? 건물 뿌수나요? ㅠㅠ   @군산 옛군산세관다시 박물관쪽으로 돌아와서 옛군산세관!바로 옆은 지금의 군산세관이 있다ㅋㅋㅋ   @군산 영화원첫날 일정 다 끝나고나니 5시 좀 넘는 시간 ,, 숙소갔다가 다시 오기도 애매한 거리라그냥 일찍 저녁 먹기로 결정 !!!!군산은 지린성 고추짜장이 유명하지만매운거 잘 먹는 사람도 맵다고 할 정도라바로 포기하고 대신 물짜장을 먹어보기로 함.군산 물짜장은 빈해원/ 복성루/ 영화원 이렇게 3군데 많이 나오는데 빈해원과 복성루는맑은 물짜장이고 내가 간 영화원은 빨갛다!맛은? 쫀맛탱!!!! 물짜장도 내 입에 잘 맞았다약간 마파두부?같은? 전혀 맵지않고 달달해서좀 더 매콤해도 더 맛있을거 같은 느낌!또 먹으러 가고싶다 ㅠㅠ    숙소 돌아가는 길.해 지는 초원사진관도 넘 예뻐서 😌   하얀달 🌙   숙소 도차아아악수고했다 ! !!!   이거슨 @군산 마이페이보릿에서 구매한것.내 최애영화 비포선라이즈 엽서랑보헤미안랩소디는 퀸 짱팬인 아빠도 주려고 2장!그리고 보헤미안 카세트테이프도 아빠선물인데문제는 아빠 차 cd만 되는거...몰랐다고 한다...집에 카세트 플레이어도 없는데요!!! 광광! 그래도 나름 구하기 힘들거나 비싸게 파는듯해서그냥 그렇게 위안 삼는중ㅇㅇ....퀸 최고❤️   다음 날.준비 다 끝내고 조식 기다리며 사진 찰칵찰칵      난 이 자리가 마음에 들었다 히히 맛이 없을수가 없지!!!!벗 토마토를 못먹어서 다 못먹은게 죄송했다😢   @군산 동국사마지막 날 일정은 동국사와 군산항쟁관 방문.11시 체크아웃까지 시간이 남아서짐은 숙소에 두고 동국사로 왔다!   한국에 유일하게 남아있는 일본식 사찰 동국사.종교가 불교라 어릴때부터 여러 절 많이 다녔는데확실히 대웅전부터 일본식 사찰 느낌이라기분이 뭔가 묘했다 .. !      동국사에는 일제강점기 시절의 잘못을 참회하는 참사비와 위안부 소녀상도 있었다   동국사 냥이 두마리🐱 귀 여 워 🐱      오래있을 분위기는 아니어서 잠깐 구경하고동국사 입구에 계시는 관광안내소 직원분께군산항쟁관 위치를 확인차 여쭤보니아주 친절히 가르쳐주셨다 😊            @군산 군산항쟁관35년간 군산의 항쟁역사를 보여주는 공간.2층에는 참혹한 고문현장과 고문체험 위주라나는 1층 사진만 조금 남기다 말았다참으로 숙연해지는 곳...🙏🏻   숙소 체크아웃 후,초원사진관 앞에서 사진도 한방 남기고!감사합니다^^7❤️❤️❤️         @군산 장미칼국수걸어서 장미칼국수 본점으로..!내가 진짜 엄청난 칼국수 킬런데진짜 거의 다 남겼다...ㅋㅋ모르겠다 내 입엔 정말 노맛이었음 ㅠㅠ 남기는게 죄송해서라도 최대한 먹어볼랬는데양이 줄지를 않아!!!!!!근데 여기 김치 맛집임김치는 진짜 인정..ㅋㅋㅋㅋㅋㅋㅋ암튼 결론은 칼국수는동래시장 3000원 칼국수가 최고^^77   택시타고 터미널로 넘어와서1시 40분 차타고 부산으로 ~~~ 😭잘있어 군산!   또 2시간 잘 자다가 휴게소 다왔을때쯤 깨서나머지 2시간은 쭈욱 눈뜬채 왔다세차 함 해주이소!    부산 도착해서 집가는길 계란빵 팔길래바로 사버림 ㅠㅠ 내사랑 계란빵 🍳🍳   집 도착하자마자 기다리던 택배 뜯어보고 ㅋ   이성당 봉툰데 크리스마스 느낌 낭낭집오니 피곤함 쏟아지는 와중에도 사진 남김아 맞다 이성당 단팥빵도 맛있었으요 !!! ❤️💚   이렇게 마무리되는 군산여행 포스팅!예전부터 군산 너무 가보고 싶었는데5년만에 혼자 다녀온 국내여행도 대성공이었다뚜벅이에게는 최고의 여행지가 아닐까 하는 군산해외여행도 좋지만 국내도 좋아요!!!! (*˘◡˘*)  '

현재, 본문 내용 중 이모티콘이 제거되지 않았습니다. 아래에서 한글만을 남기고 다른 문자는 모두 제거하겠습니다.

2.3 한글 추출

In [9]:
# 내용 중 한글만을 추출하는 함수를 만들겠습니다.

def extract_hangeul(x):
    han_contents = []

    for i in contents:
        text = re.sub('[^ㄱ-ㅎㅏ-ㅣ가-힣 ]',' ',i).lstrip()

        if(text != ''):
            han_contents.append(text)

    return han_contents

han_contents = extract_hangeul(contents)

han_contents[1]
Out[9]:
'여자 혼자  박 일 군산 국내여행        칸쵸                          복사 이웃추가본문 기타 기능번역보기   여자 혼자  박 일 군산 국내여행            숙소에서 잠시 쉬고 다시 나와  군산근대역사박물관 으로  박물관 건축관 미술관 비응함 입장가능한통합권은 박물관 입장권이랑     원 차이난다 통합권     원   박물관 입장권     원 하지만 나는 박물관만 관람        층 근대생활관이렇게 옛날 교복이나 옷 같은거피팅해볼수 있어서 모자라도 한번 써봄 ㅋ이 비슷한거 나도 있긴 한데      난 어릴때부터 아기고무신을 완   전귀여워했다  물론 지금도  근데 여기 있어서 또 심쿵   ㅇ      저기 또 있어 엉엉    아니 또ㅠㅠㅠㅠㅠ너무 좋아서 신발 사진만  장 남겼나보다     아픈 역사의 순간들이지만확실히  층 근대생활관이 사람들의 관심이제일 컸던거 같다  나역시     층은     년대의 모습을 재현해둔곳이라 모든게 다 신기했다  이리 익산이래용     군산 부잔교 뜬다리부두 박물관 바로 옆이라부잔교 진포해양테마공원 구경할겸 왔다 저 다리 한번 건너가보고싶었는데일반인은 가지말라고해서 말 잘 듣는 일반인  인 그냥 여기서 사진만 찰칵 ㅋㅋㅋ이 다리들도 일제강점기 시절 일본에 쌀을 보내기 위해 만들었단다          물이 빠진 상태라 신기했다 마음은 선유도에 가고싶었지만 넘 멀어서이렇게나마 군산 바다 구경    바다있는 지역에서 나고 자랐지만넘 좋았다 속이 뻥ㅇ                      군산 진포해양테마공원뭐 일부러 보러갈정도는 아니지만나는 나름 흥미로웠다 ㅋㅋㅋ         두번째 사진에서 보이는 군산    카페에많이들 가지만 나는 근처 건물 외관들 구경만     아마도 비행기 두대           안녕 나야   원래 태극기 전체가 있는걸로 아는데왜 윗부분이 없는걸까  건물 뿌수나요  ㅠㅠ    군산 옛군산세관다시 박물관쪽으로 돌아와서 옛군산세관 바로 옆은 지금의 군산세관이 있다ㅋㅋㅋ    군산 영화원첫날 일정 다 끝나고나니  시 좀 넘는 시간    숙소갔다가 다시 오기도 애매한 거리라그냥 일찍 저녁 먹기로 결정     군산은 지린성 고추짜장이 유명하지만매운거 잘 먹는 사람도 맵다고 할 정도라바로 포기하고 대신 물짜장을 먹어보기로 함 군산 물짜장은 빈해원  복성루  영화원 이렇게  군데 많이 나오는데 빈해원과 복성루는맑은 물짜장이고 내가 간 영화원은 빨갛다 맛은  쫀맛탱     물짜장도 내 입에 잘 맞았다약간 마파두부 같은  전혀 맵지않고 달달해서좀 더 매콤해도 더 맛있을거 같은 느낌 또 먹으러 가고싶다 ㅠㅠ    숙소 돌아가는 길 해 지는 초원사진관도 넘 예뻐서     하얀달     숙소 도차아아악수고했다         이거슨  군산 마이페이보릿에서 구매한것 내 최애영화 비포선라이즈 엽서랑보헤미안랩소디는 퀸 짱팬인 아빠도 주려고  장 그리고 보헤미안 카세트테이프도 아빠선물인데문제는 아빠 차   만 되는거   몰랐다고 한다   집에 카세트 플레이어도 없는데요    광광  그래도 나름 구하기 힘들거나 비싸게 파는듯해서그냥 그렇게 위안 삼는중ㅇㅇ    퀸 최고     다음 날 준비 다 끝내고 조식 기다리며 사진 찰칵찰칵      난 이 자리가 마음에 들었다 히히 맛이 없을수가 없지    벗 토마토를 못먹어서 다 못먹은게 죄송했다     군산 동국사마지막 날 일정은 동국사와 군산항쟁관 방문   시 체크아웃까지 시간이 남아서짐은 숙소에 두고 동국사로 왔다    한국에 유일하게 남아있는 일본식 사찰 동국사 종교가 불교라 어릴때부터 여러 절 많이 다녔는데확실히 대웅전부터 일본식 사찰 느낌이라기분이 뭔가 묘했다           동국사에는 일제강점기 시절의 잘못을 참회하는 참사비와 위안부 소녀상도 있었다   동국사 냥이 두마리  귀 여 워        오래있을 분위기는 아니어서 잠깐 구경하고동국사 입구에 계시는 관광안내소 직원분께군산항쟁관 위치를 확인차 여쭤보니아주 친절히 가르쳐주셨다               군산 군산항쟁관  년간 군산의 항쟁역사를 보여주는 공간  층에는 참혹한 고문현장과 고문체험 위주라나는  층 사진만 조금 남기다 말았다참으로 숙연해지는 곳        숙소 체크아웃 후 초원사진관 앞에서 사진도 한방 남기고 감사합니다                   군산 장미칼국수걸어서 장미칼국수 본점으로   내가 진짜 엄청난 칼국수 킬런데진짜 거의 다 남겼다   ㅋㅋ모르겠다 내 입엔 정말 노맛이었음 ㅠㅠ 남기는게 죄송해서라도 최대한 먹어볼랬는데양이 줄지를 않아      근데 여기 김치 맛집임김치는 진짜 인정  ㅋㅋㅋㅋㅋㅋㅋ암튼 결론은 칼국수는동래시장     원 칼국수가 최고       택시타고 터미널로 넘어와서 시   분 차타고 부산으로      잘있어 군산    또  시간 잘 자다가 휴게소 다왔을때쯤 깨서나머지  시간은 쭈욱 눈뜬채 왔다세차 함 해주이소     부산 도착해서 집가는길 계란빵 팔길래바로 사버림 ㅠㅠ 내사랑 계란빵      집 도착하자마자 기다리던 택배 뜯어보고 ㅋ   이성당 봉툰데 크리스마스 느낌 낭낭집오니 피곤함 쏟아지는 와중에도 사진 남김아 맞다 이성당 단팥빵도 맛있었으요           이렇게 마무리되는 군산여행 포스팅 예전부터 군산 너무 가보고 싶었는데 년만에 혼자 다녀온 국내여행도 대성공이었다뚜벅이에게는 최고의 여행지가 아닐까 하는 군산해외여행도 좋지만 국내도 좋아요              '

한글을 제외한 모든 문자가 제거되었습니다.

In [10]:
# 본 프로젝트에서는 KoNLPy에서 제공하는 다섯 가지의 자연어처리기 중 은전한잎(Mecab)을 사용하겠습니다.

mecab = Mecab()

2.4 Stop Word

In [11]:
# stop word(불용어)를 지정하겠습니다.
# stop word에는 혼자와는 반대되는 관계 등의 단어(가족, 친구 등), 조사, 이외 본 프로젝트 목적과 관련없는 단어를 포함시켰습니다. 

stop_words = '이웃 추가 환영 댓글 공감 본문 기타 복사 기능 번역 이웃추가본문 박일 기타 복사 기능지도로 기능번역보기 \
지도닫기 전체지도 번역보기 몸 뭐 한국 빠 픽 힙 동안 가능 여행지 모습 어디 마음 가족 위치 기능 추가 지도 기타 이용 위치 \
장소 복사 본문 번역 우리 도착 이번 여기 하나 정도 다음 이곳 느낌 때문 코레 끼 플라 이젠 아무것 앤 림 순 노 뭘 유럽 부 동행 팩\
족 빈이 화 조 부분 뭐 안녕 밖 뭔가 백 경우 적 샷 해외여행 해외 만큼 땐 텐데 맘 겁니다 그때 난 무엇 이게 지 살 반 일본 이건 완전 던분 \
신랑 이거 너 얼마 선 간 턴 곳곳 켄싱 나중 애 대부분 오 언니 부모 이것 밑 땅 겸 거기 건지 조금 관련 색 그곳 여러분 커플 이후 마련 덕분\
상세 자기 외 둘째 힘 누구 그것 씨 결국 테 컷 그 저기 운 급 미 등등 초 세 핫 관 포함 연인 도 팀 여긴 당시 지인 리뷰 면 법 척 뜻\
공 떼 탄 건데 바 열 할머니 요기 군데 뿐 찜 뻔 ㄷ 식 덕 텐데요 셋 그게 꺼 포 채 청 굿 칸 어딜 주요 아저씨 막 유 용 실 병 정 권 킹\
쪽 존 군 빈 걸 저희 점 명 호 대 둘 주 남 후 제 안 앞 뒤 건 데 천 층 편 끝 줄 옆 속 위 아래 네 내 게 년 날 중 듯 은 이 것 등 더 를\
좀 즉 인 옹 때 만 원 이때 개 일 기 시 럭 갤 성 삼 스 폰 트 드 기 이 리 사 전 마 자 플 가 박 짱 어머니 아버지 엄마 아빠 동생 아들 딸\
아기 아가 아이 분 남편 이웃 수 곳 거 번 월 저 나 한때 쓰리 현과 현정 쓰 무튼 주노 그날 웡 가지 확인 저장 아기 현빈'

stop_words = stop_words.split(' ')
stop_words[0:10]
Out[11]:
['이웃', '추가', '환영', '댓글', '공감', '본문', '기타', '복사', '기능', '번역']

2.5 품사 추출

In [12]:
# 형태소 분석은 단어가 의미하는 형태를 찾아주어 원형에 대한 부분만 추출할 수 있습니다.
# pos 함수를 사용해 품사를 추출하겠습니다.
# 품사 추출 함수를 만들어 출력하겠습니다.

def extract_pos(x):
    words = []
    for content in x:
        words.extend(mecab.pos(content))  # tagger의 품사(part of speech : POS)
    return words
    
word_pos = extract_pos(han_contents)
word_pos[:10]
Out[12]:
[('여행', 'NNG'),
 ('박', 'NNBC'),
 ('일', 'NR'),
 ('국내', 'NNG'),
 ('여행', 'NNG'),
 ('양양', 'NNP'),
 ('여행', 'NNG'),
 ('에', 'JKB'),
 ('어', 'IC'),
 ('비앤비', 'NNP')]

2.5.1 명사

In [13]:
# 포스팅 글(contents)에서 명사를 추출하는 함수를 만들어 출력하겠습니다.
# 이때, stop_words 리스트에 없는 명사만 nouns 리스트에 담겠습니다. 

def extract_noun(x):
    nouns = []
    for content in x:
        for noun in mecab.nouns(content):
            if noun not in stop_words:
                nouns.append(noun)
    return nouns
nouns = extract_noun(han_contents)
nouns[:10]
Out[13]:
['여행', '국내', '여행', '양양', '여행', '비앤비', '서피', '비치', '국내', '여행']

2.5.2 동사

In [14]:
# 동사를 추출하는 함수를 만들겠습니다.
# 추출을 위해 형태소 분석을 한 결과에서 동사(VV)에 해당하는 형태에 '다'를 추가하겠습니다.

def extract_verb(x):
    verbs = []
    for word in x:
        if word[1] == 'VV':
            verbs.append(word[0] + '다')
    return verbs
        
verbs = extract_verb(word_pos)
verbs[0:10]
Out[14]:
['보다', '떠나다', '하다', '태어나다', '보다', '하다', '잡다', '있다', '보내다', '가다']

2.5.3 형용사

In [15]:
# 형용사를 추출하는 함수를 만들겠습니다.
# 추출을 위해 형태소 분석을 한 결과에서 형용사(VA)에 해당하는 형태에 '다'를 추가하겠습니다.

def extract_adj(x):
    adjective = []
    for word in x:
        if word[1] == 'VA':
            adjective.append(word[0] + '다')
    return adjective

adjective = extract_adj(word_pos)
adjective[0:10]
Out[15]:
['좋다', '예쁘다', '좋다', '같다', '없다', '있다', '있다', '있다', '좋다', '괜찮다']
In [16]:
# 300개(임의 지정)의 단어에 대한 빈도를 추출하겠습니다.

num_top_nouns = 300
travel_noun_count = Counter(nouns)

# most_common() : 데이터의 개수가 많은 순으로 정렬된 배열을 리턴하는 메서드
travel_top_nouns = dict(travel_noun_count.most_common(num_top_nouns))

sorted(travel_top_nouns.items(), key=lambda x: x[1], reverse=True)[:10]
Out[16]:
[('여행', 9933),
 ('국내', 4273),
 ('사진', 2763),
 ('시간', 2237),
 ('길', 1923),
 ('생각', 1845),
 ('사람', 1645),
 ('혼자', 1579),
 ('추천', 1283),
 ('바다', 1267)]

추출된 명사의 빈도별로 살펴보면, 여행, 국내, 사진, 시간, 길, 생각 등의 단어의 빈도가 가장 높았습니다.

3. 자연어처리

3.1 BOW(Bag Of Word)

  • 군집을 만들기 위해 가장 적절한 방법은 게시물마다 등장하는 단어의 빈도수를 파악해 하나의 카운트 벡터로 만듭니다. 이를 단어 주머니 접근 법이라고 합니다. 카운트 벡터 생성 후 해당 게시물과 다른 게시물 사이의 벡터 거리를 계산하여 게시물 사이의 유사도를 파악하면 됩니다.
In [17]:
# BOW 단어 가방에 단어를 토큰화해서 담겠습니다.
# 기본적으로 CountVectorizer 는 1글자 단어를 제거하여 출력합니다.

from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer(analyzer = 'word',        # 단어 단위로 벡터화
                            tokenizer = None,         # 별도의 토크나이저를 지정하지 않음
                            preprocessor = None,      # 전처리 도구
                            stop_words = stop_words,  # 불용어 지정
                            max_features = 50000)     # 만들 단어의 수 50000개

print(vectorizer.fit_transform(travel_top_nouns).toarray()) 
print(sorted(vectorizer.vocabulary_.items()))
[[0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 ...
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]]
[('가격', 0), ('가을', 1), ('감사', 2), ('감상', 3), ('강릉', 4), ('강원도', 5), ('개인', 6), ('거리', 7), ('걱정', 8), ('건물', 9), ('검색', 10), ('게스트', 11), ('겨울', 12), ('경주', 13), ('경치', 14), ('경험', 15), ('계곡', 16), ('계단', 17), ('계절', 18), ('계획', 19), ('고기', 20), ('고민', 21), ('공간', 22), ('공원', 23), ('공항', 24), ('관광', 25), ('관광객', 26), ('관광지', 27), ('관람', 28), ('관심', 29), ('구경', 30), ('구매', 31), ('구입', 32), ('국내', 33), ('군산', 34), ('규모', 35), ('그림', 36), ('근처', 37), ('기대', 38), ('기분', 39), ('기억', 40), ('기차', 41), ('나라', 42), ('나름', 43), ('나무', 44), ('낚시', 45), ('날씨', 46), ('남자', 47), ('내부', 48), ('내일', 49), ('네이버', 50), ('녹원', 51), ('놀이', 52), ('다리', 53), ('다양', 54), ('단양', 55), ('단풍', 56), ('담양', 57), ('당일치기', 58), ('대구', 59), ('대전', 60), ('대표', 61), ('덕분', 62), ('도시', 63), ('동네', 64), ('동해', 65), ('드라마', 66), ('리조트', 67), ('마무리', 68), ('마을', 69), ('마지막', 70), ('만족', 71), ('맛집', 72), ('매력', 73), ('맥주', 74), ('메뉴', 75), ('명소', 76), ('목장', 77), ('무료', 78), ('문화', 79), ('바다', 80), ('바람', 81), ('바위', 82), ('바퀴', 83), ('박물관', 84), ('방문', 85), ('방법', 86), ('버스', 87), ('벚꽃', 88), ('벽화', 89), ('부산', 90), ('분위기', 91), ('블로그', 92), ('사람', 93), ('사랑', 94), ('사용', 95), ('사이', 96), ('사장', 97), ('사진', 98), ('사찰', 99), ('산책', 100), ('새벽', 101), ('생각', 102), ('서울', 103), ('선택', 104), ('설명', 105), ('세계', 106), ('세상', 107), ('세트', 108), ('소개', 109), ('소리', 110), ('소원', 111), ('속초', 112), ('수목원', 113), ('수영장', 114), ('숙박', 115), ('숙소', 116), ('순간', 117), ('순천', 118), ('스카이', 119), ('스타', 120), ('시간', 121), ('시기', 122), ('시대', 123), ('시설', 124), ('시작', 125), ('시장', 126), ('식당', 127), ('식사', 128), ('실내', 129), ('아침', 130), ('안내', 131), ('안면', 132), ('야경', 133), ('야외', 134), ('언덕', 135), ('여름', 136), ('여수', 137), ('여유', 138), ('여자', 139), ('여행', 140), ('역사', 141), ('연휴', 142), ('영상', 143), ('영화', 144), ('예약', 145), ('예전', 146), ('오늘', 147), ('오랜만', 148), ('오후', 149), ('올해', 150), ('요즘', 151), ('운영', 152), ('운전', 153), ('울산', 154), ('월드', 155), ('유명', 156), ('음식', 157), ('의미', 158), ('이날', 159), ('이동', 160), ('이름', 161), ('이상', 162), ('이야기', 163), ('이유', 164), ('인기', 165), ('인생', 166), ('인천', 167), ('일반', 168), ('일상', 169), ('일정', 170), ('일출', 171), ('입구', 172), ('입장', 173), ('입장료', 174), ('자리', 175), ('자연', 176), ('자전거', 177), ('작년', 178), ('재미', 179), ('저녁', 180), ('전국', 181), ('전망대', 182), ('전주', 183), ('전체', 184), ('전화', 185), ('점심', 186), ('정리', 187), ('정보', 188), ('정원', 189), ('제주', 190), ('제주도', 191), ('제천', 192), ('조식', 193), ('주말', 194), ('주문', 195), ('주변', 196), ('주차', 197), ('주차장', 198), ('준비', 199), ('중간', 200), ('지금', 201), ('지역', 202), ('진행', 203), ('참고', 204), ('처음', 205), ('체험', 206), ('촬영', 207), ('최고', 208), ('추석', 209), ('추억', 210), ('추천', 211), ('축제', 212), ('춘천', 213), ('출발', 214), ('출처', 215), ('친구', 216), ('카메라', 217), ('카페', 218), ('캐리어', 219), ('캠핑', 220), ('커피', 221), ('케이블카', 222), ('코로나', 223), ('코스', 224), ('태안', 225), ('택시', 226), ('터널', 227), ('터미널', 228), ('통영', 229), ('투어', 230), ('특별', 231), ('파도', 232), ('패스', 233), ('펜션', 234), ('평일', 235), ('평창', 236), ('포스팅', 237), ('포토', 238), ('포항', 239), ('폭포', 240), ('풍경', 241), ('플레이스', 242), ('필요', 243), ('하늘', 244), ('하루', 245), ('하우스', 246), ('한옥마을', 247), ('한잔', 248), ('할인', 249), ('해변', 250), ('해수욕장', 251), ('해운대', 252), ('행복', 253), ('호텔', 254), ('혼자', 255), ('홈페이지', 256), ('화장실', 257), ('후기', 258), ('휴가', 259), ('힐링', 260)]
In [18]:
# vectorizer가 travel_top_nouns 를 학습하도록 하겠습니다.

travel_top_nouns_vector = vectorizer.fit(travel_top_nouns)

travel_top_nouns_vector
Out[18]:
CountVectorizer(max_features=50000,
                stop_words=['이웃', '추가', '환영', '댓글', '공감', '본문', '기타', '복사',
                            '기능', '번역', '이웃추가본문', '박일', '기타', '복사', '기능지도로',
                            '기능번역보기', '지도닫기', '전체지도', '번역보기', '몸', '뭐', '한국',
                            '빠', '픽', '힙', '동안', '가능', '여행지', '모습', '어디', ...])
In [19]:
# 변환된 travel_top_nouns 261개의 단어 * 982개의 문서 간의 array 형태를 DataFrame 형태로 출력하겠습니다. 

doc_word_df = pd.DataFrame(travel_top_nouns_vector.transform(contents).toarray())

doc_word_df
Out[19]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ... 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 2 0 0 0 1 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 ... 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 0 0 0
2 1 0 0 0 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 4 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 1 1 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
977 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
978 0 0 0 0 0 2 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 ... 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
979 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
980 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 1 0 0 0 0 0 0
981 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

982 rows × 261 columns

In [20]:
# get_feature_names() 메소드를 통해 생성한 피처를 가져와 출력하겠습니다.
vocab = vectorizer.get_feature_names()  
print(len(vocab))

# 261개의 단어와 인덱스 값을 쌍으로 출력하겠습니다.
vocab_dict = vectorizer.vocabulary_ 
sorted(vocab_dict.items(), key=lambda x: x[1], reverse=False)[:10]
261
Out[20]:
[('가격', 0),
 ('가을', 1),
 ('감사', 2),
 ('감상', 3),
 ('강릉', 4),
 ('강원도', 5),
 ('개인', 6),
 ('거리', 7),
 ('걱정', 8),
 ('건물', 9)]
In [21]:
# 위의 행렬에서 각 컬럼 숫자와 매핑되는 단어로 변환하겠습니다.
# 각 블로그 본문마다 등장하는 단어의 빈도를 출력하겠습니다.

vocab_df = pd.DataFrame(travel_top_nouns_vector.transform(contents).toarray(), columns = vocab)

vocab_df
Out[21]:
가격 가을 감사 감상 강릉 강원도 개인 거리 걱정 건물 검색 게스트 겨울 경주 경치 경험 계곡 계단 계절 계획 고기 고민 공간 공원 공항 ... 평창 포스팅 포토 포항 폭포 풍경 플레이스 필요 하늘 하루 하우스 한옥마을 한잔 할인 해변 해수욕장 해운대 행복 호텔 혼자 홈페이지 화장실 후기 휴가 힐링
0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 2 0 0 0 1 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 ... 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 0 0 0
2 1 0 0 0 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 4 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 1 1 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
977 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
978 0 0 0 0 0 2 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 ... 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
979 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
980 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 1 0 0 0 0 0 0
981 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

982 rows × 261 columns

In [22]:
# 위에서 구한 단어벡터를 더하면 단어가 전체에서 등장하는 횟수를 알 수 있습니다.
# 벡터화된 피처를 확인하여 Bag Of Word에 저장된 단어 빈도를 확인하겠습니다.

df_frequency = pd.DataFrame(np.sum(vocab_df))

df_frequency.columns = ['freq']

print(len(df_frequency))
df_frequency.sort_values(by = 'freq', ascending = False).head(20)
261
Out[22]:
freq
여행 1374
혼자 1185
사진 669
국내 604
추천 469
제주도 324
경주 292
강원도 291
여수 254
처음 249
카페 246
나름 241
요즘 239
시간 231
부산 226
바다 215
지금 212
맛집 211
호텔 210
강릉 204
In [23]:
# 위의 행렬에서 가장 빈도가 높은 단어와 빈도를 출력하겠습니다.

max_freq = 0
word = None

for i in range(len(df_frequency['freq'])):
    if df_frequency['freq'][i] > max_freq:
        max_freq = df_frequency['freq'][i]
        word = list(df_frequency.index)[i]

print(f'가장 빈도가 높은 단어 : {word}, 빈도 : {max_freq}')
가장 빈도가 높은 단어 : 여행, 빈도 : 1374

Counter 라이브러리의 mecab.nouns() 함수를 사용했을 때의 결과는 복합명사(양양여행)를 하나로 보지 않고 '양양'과 '여행'으로 따로 빈도를 세었기 때문에 BOW 빈도와는 차이가 있습니다. 본 프로젝트에서는 Counter 라이브러리 활용 결과는 단순 상위 300개의 빈도만을 추출하기 위한 목적으로 사용되었습니다.

BOW 처리 과정에서는 300개의 단어를 활용하여 해당 단어가 블로그 내용 내에 몇 번 등장했는지를 기초로 매트릭스가 출력되었습니다.

  • BOW 결과를 간략히 정리하면 다음과 같습니다.
- 1. 빈도가 가장 높은 단어는 여행(1374)으로 나타났습니다. 다음으로, 혼자(1185), 사진(669), 국내(604), 추천(469) 등의 순으로 빈도가 높았습니다.

- 2. 자주 등장한 지역을 살펴보면, 제주도(324)가 가장 많았습니다. 다음으로, 경주(292), 강원도(291), 여수(254), 부산(226), 강릉(204) 등으로 나타났습니다.

3.2 어휘 빈도 - 문서 역빈도(TF-IDF)

  • TF : 특정한 단어가 현재 문서 내에 얼마나 자주 등장하는지(단어의 빈도수)를 나타낸 값으로, 이 값이 높을수록 문서에서 중요하다고 생각하는 개념입니다.
  • 빈도가 높다고 텍스트의 주제를 잘 드러내는 핵심어라고 보기 어렵습니다. 그 어휘가 다른 문서에는 별로 등장하지 않고 특정 문서에만 집중적으로 등장 할 때 그 어휘야말로 실질적으로 그 문서의 주제를 잘 담고 있는 핵심어라 할 수 있습니다. 특정 문서에서 특정 단어가 많이 등장하는 빈도가 많으면서 동시에 그 단어가 다른 문서에서 등장하는 빈도가 적을 때, 특정 문서의 핵심어로 간주합니다.
  • 단어 자체가 전체 문서 내에서 자주 사용되는 경우, 이 단어는 흔하게 등장하는 것을 의미하는 데, 이것을 단어가 나타난 문서의 수 DF(Documnet Frequency, 문서 빈도)라고 하며, IDF는 이 값의 역수를 의미합니다.
  • TF-IDF 값은 특정 단어의 상대적인 빈도수를 나타내 주는 값이다. 값이 클 수록 현재 문서에서는 자주 언급되면서 다른 문서에서는 잘 언급되지 않음을 뜻하고, 값이 작을수록 다른 문서에는 자주 언급되면서 현재 문서와 관련성이 낮음을 의미합니다.
  • 핵심어는 특정 문서에서 특정 단어가 많이 등장하는 어휘빈도(TF)와 다른 문서에서 등장하지 않는 문서 역빈도(IDF)의 곱을 통해 추출합니다.
In [24]:
# 특징 추출 방법으로 TF-IDF 값을 사용할 경우, 단순 횟수를 이용하는 것보다 각 단어의 특성을 좀 더 잘 반영함으로 
# Countvectorizer보다 더 좋은 결과를 만들어낼 수 있습니다.

# TF-IDF를 구하기 위해서는 TfidfVectorizer 라이브러리를 설치해야 합니다.

from sklearn.feature_extraction.text import TfidfVectorizer

# TfidfVectorizer() 통해 문서 내에서 특정 단어의 중요도를 구하겠습니다.
# 빈도가 높은 300개의 단어의 벡터화를 진행하겠습니다.
travel_tfidv = TfidfVectorizer().fit(travel_top_nouns)

# 추출한 명사가 블로그 내용에서 어떤 TF-IDF 값을 가지는지 배열 형태로 출력하겠습니다.
print(travel_tfidv.transform(contents).toarray())
print(travel_tfidv)
[[0.         0.         0.         ... 0.         0.         0.        ]
 [0.         0.         0.         ... 0.         0.         0.        ]
 [0.12909944 0.         0.         ... 0.         0.         0.        ]
 ...
 [0.         0.18569534 0.         ... 0.         0.         0.        ]
 [0.         0.         0.         ... 0.         0.         0.        ]
 [0.         0.         0.         ... 0.         0.         0.        ]]
TfidfVectorizer()
In [25]:
# 단어사전을 출력하겠습니다.
word_tfidv = travel_tfidv.vocabulary_

# TF-IDF 값을 DataFrame 형식으로 만들겠습니다.
df_tfidv = pd.DataFrame(travel_tfidv.transform(contents).toarray())

# 각 단어에 부여된 인덱스를 기준으로 정렬해서 단어사전을 출력하겠습니다.
print(sorted(word_tfidv.items()))
df_tfidv
[('가격', 0), ('가을', 1), ('감사', 2), ('감상', 3), ('강릉', 4), ('강원도', 5), ('개인', 6), ('거리', 7), ('걱정', 8), ('건물', 9), ('검색', 10), ('게스트', 11), ('겨울', 12), ('경주', 13), ('경치', 14), ('경험', 15), ('계곡', 16), ('계단', 17), ('계절', 18), ('계획', 19), ('고기', 20), ('고민', 21), ('공간', 22), ('공원', 23), ('공항', 24), ('관광', 25), ('관광객', 26), ('관광지', 27), ('관람', 28), ('관심', 29), ('구경', 30), ('구매', 31), ('구입', 32), ('국내', 33), ('군산', 34), ('규모', 35), ('그림', 36), ('근처', 37), ('기대', 38), ('기분', 39), ('기억', 40), ('기차', 41), ('나라', 42), ('나름', 43), ('나무', 44), ('낚시', 45), ('날씨', 46), ('남자', 47), ('내부', 48), ('내일', 49), ('네이버', 50), ('녹원', 51), ('놀이', 52), ('다리', 53), ('다양', 54), ('단양', 55), ('단풍', 56), ('담양', 57), ('당일치기', 58), ('대구', 59), ('대전', 60), ('대표', 61), ('덕분', 62), ('도시', 63), ('동네', 64), ('동해', 65), ('드라마', 66), ('리조트', 67), ('마무리', 68), ('마을', 69), ('마지막', 70), ('만족', 71), ('맛집', 72), ('매력', 73), ('맥주', 74), ('메뉴', 75), ('명소', 76), ('목장', 77), ('무료', 78), ('문화', 79), ('바다', 80), ('바람', 81), ('바위', 82), ('바퀴', 83), ('박물관', 84), ('방문', 85), ('방법', 86), ('버스', 87), ('벚꽃', 88), ('벽화', 89), ('부산', 90), ('분위기', 91), ('블로그', 92), ('사람', 93), ('사랑', 94), ('사용', 95), ('사이', 96), ('사장', 97), ('사진', 98), ('사찰', 99), ('산책', 100), ('새벽', 101), ('생각', 102), ('서울', 103), ('선택', 104), ('설명', 105), ('세계', 106), ('세상', 107), ('세트', 108), ('소개', 109), ('소리', 110), ('소원', 111), ('속초', 112), ('수목원', 113), ('수영장', 114), ('숙박', 115), ('숙소', 116), ('순간', 117), ('순천', 118), ('스카이', 119), ('스타', 120), ('시간', 121), ('시기', 122), ('시대', 123), ('시설', 124), ('시작', 125), ('시장', 126), ('식당', 127), ('식사', 128), ('실내', 129), ('아침', 130), ('안내', 131), ('안면', 132), ('야경', 133), ('야외', 134), ('언덕', 135), ('여름', 136), ('여수', 137), ('여유', 138), ('여자', 139), ('여행', 140), ('역사', 141), ('연휴', 142), ('영상', 143), ('영화', 144), ('예약', 145), ('예전', 146), ('오늘', 147), ('오랜만', 148), ('오후', 149), ('올해', 150), ('요즘', 151), ('운영', 152), ('운전', 153), ('울산', 154), ('월드', 155), ('유명', 156), ('음식', 157), ('의미', 158), ('이날', 159), ('이동', 160), ('이름', 161), ('이상', 162), ('이야기', 163), ('이유', 164), ('인기', 165), ('인생', 166), ('인천', 167), ('일반', 168), ('일상', 169), ('일정', 170), ('일출', 171), ('입구', 172), ('입장', 173), ('입장료', 174), ('자리', 175), ('자연', 176), ('자전거', 177), ('작년', 178), ('재미', 179), ('저녁', 180), ('전국', 181), ('전망대', 182), ('전주', 183), ('전체', 184), ('전화', 185), ('점심', 186), ('정리', 187), ('정보', 188), ('정원', 189), ('제주', 190), ('제주도', 191), ('제천', 192), ('조식', 193), ('주말', 194), ('주문', 195), ('주변', 196), ('주차', 197), ('주차장', 198), ('준비', 199), ('중간', 200), ('지금', 201), ('지역', 202), ('진행', 203), ('참고', 204), ('처음', 205), ('체험', 206), ('촬영', 207), ('최고', 208), ('추석', 209), ('추억', 210), ('추천', 211), ('축제', 212), ('춘천', 213), ('출발', 214), ('출처', 215), ('친구', 216), ('카메라', 217), ('카페', 218), ('캐리어', 219), ('캠핑', 220), ('커피', 221), ('케이블카', 222), ('코로나', 223), ('코스', 224), ('태안', 225), ('택시', 226), ('터널', 227), ('터미널', 228), ('통영', 229), ('투어', 230), ('특별', 231), ('파도', 232), ('패스', 233), ('펜션', 234), ('평일', 235), ('평창', 236), ('포스팅', 237), ('포토', 238), ('포항', 239), ('폭포', 240), ('풍경', 241), ('플레이스', 242), ('필요', 243), ('하늘', 244), ('하루', 245), ('하우스', 246), ('한옥마을', 247), ('한잔', 248), ('할인', 249), ('해변', 250), ('해수욕장', 251), ('해운대', 252), ('행복', 253), ('호텔', 254), ('혼자', 255), ('홈페이지', 256), ('화장실', 257), ('후기', 258), ('휴가', 259), ('힐링', 260)]
Out[25]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ... 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
0 0.000000 0.000000 0.0 0.0 0.000000 0.369274 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.123091 0.123091 0.0 0.246183 0.0 0.0 0.000000 0.123091 0.0 0.0 0.0 0.0 0.0
1 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.123091 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.061546 0.0 0.0 ... 0.000000 0.061546 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.184637 0.0 0.0 0.0 0.0 0.0
2 0.129099 0.000000 0.0 0.0 0.258199 0.258199 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.516398 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.129099 0.0 0.0 0.000000 0.129099 0.0 0.000000 0.0 0.0 0.129099 0.129099 0.0 0.0 0.0 0.0 0.0
3 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.070360 0.0 0.0 0.0 0.0 0.0
4 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.412082 0.0 0.0 0.0 0.0 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
977 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.188982 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.188982 0.0 0.0 0.0 0.0 0.0
978 0.000000 0.000000 0.0 0.0 0.000000 0.294884 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.147442 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.147442 0.0 0.0 ... 0.000000 0.147442 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.147442 0.0 0.0 0.0 0.0 0.0
979 0.000000 0.185695 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.185695 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.185695 0.0 0.0 0.0 0.0 0.0
980 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.304997 0.0 0.0 0.152499 0.000000 0.0 0.0 0.0 0.0 0.0
981 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0

982 rows × 261 columns

현재, 행렬에서는 컬럼이 숫자로 출력되어 숫자가 의미하는 단어를 알기 어렵습니다. 아래에서 숫자를 단어로 변환하는 과정을 수행하겠습니다.

In [26]:
# df_tfidv 행렬의 컬럼을 숫자에서 추출한 단어로 변환시키겠습니다.
# 먼저, 컬럼에 매칭시킬 단어 리스트를 만들기 위해 (단어, 인덱스) 의 튜플형식에서 단어부분을 가져오겠습니다.

sort_word = sorted(word_tfidv.items())

sort_word_list = []
for i in sort_word:
    sort_word_list.append(i[0])

print(len(sort_word_list))  # 한 글자 단어를 제외한 총 261개의 단어가 리스트로 만들어졌습니다.
sort_word_list[:10] # 단어가 리스트 형태로 출력되었습니다.
261
Out[26]:
['가격', '가을', '감사', '감상', '강릉', '강원도', '개인', '거리', '걱정', '건물']
In [27]:
# 저장된 단어 리스트를 컬럼으로 입력하겠습니다.

df_tfidv.columns = sort_word_list

df_tfidv
Out[27]:
가격 가을 감사 감상 강릉 강원도 개인 거리 걱정 건물 검색 게스트 겨울 경주 경치 경험 계곡 계단 계절 계획 고기 고민 공간 공원 공항 ... 평창 포스팅 포토 포항 폭포 풍경 플레이스 필요 하늘 하루 하우스 한옥마을 한잔 할인 해변 해수욕장 해운대 행복 호텔 혼자 홈페이지 화장실 후기 휴가 힐링
0 0.000000 0.000000 0.0 0.0 0.000000 0.369274 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.123091 0.123091 0.0 0.246183 0.0 0.0 0.000000 0.123091 0.0 0.0 0.0 0.0 0.0
1 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.123091 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.061546 0.0 0.0 ... 0.000000 0.061546 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.184637 0.0 0.0 0.0 0.0 0.0
2 0.129099 0.000000 0.0 0.0 0.258199 0.258199 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.516398 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.129099 0.0 0.0 0.000000 0.129099 0.0 0.000000 0.0 0.0 0.129099 0.129099 0.0 0.0 0.0 0.0 0.0
3 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.070360 0.0 0.0 0.0 0.0 0.0
4 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.412082 0.0 0.0 0.0 0.0 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
977 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.188982 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.188982 0.0 0.0 0.0 0.0 0.0
978 0.000000 0.000000 0.0 0.0 0.000000 0.294884 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.147442 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.147442 0.0 0.0 ... 0.000000 0.147442 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.147442 0.0 0.0 0.0 0.0 0.0
979 0.000000 0.185695 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.185695 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.185695 0.0 0.0 0.0 0.0 0.0
980 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.304997 0.0 0.0 0.152499 0.000000 0.0 0.0 0.0 0.0 0.0
981 0.000000 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 ... 0.000000 0.000000 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.000000 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0

982 rows × 261 columns

행렬에서 컬럼이 숫자에서 단어로 변환되어 출력되었습니다. BoW 와 같이 각 단어별 총 TF-IDF 산출하겠습니다.

In [28]:
# 먼저 df_tfidv 의 컬럼을 리스트로 만들겠습니다.
df_tfidv_col_list = list(df_tfidv.columns)

# 만든 리스트를 enumerate() 함수를 활용하여 딕셔너리 형태로 만들겠습니다.
df_tfidv_col_dict = {index : word for word, index in enumerate(df_tfidv_col_list)}

# df_tfidv_col_dict 의 values 부분을 TF-IDF 총합으로 채워넣겠습니다.
for word, idx in df_tfidv_col_dict.items():
    total = df_tfidv[word].sum()
    df_tfidv_col_dict[word] = total

print(df_tfidv_col_dict)

# 만들어진 {단어 : TF-IDF} 을 DataFrame 으로 변환하고, 그것을 Transpose 하겠습니다.
tfidf_dict_df = pd.DataFrame([df_tfidv_col_dict]).T

# 컬럼이름을 TF-IDF 로 변경하겠습니다.
tfidf_dict_df.columns = ['TF-IDF']

# 만들어진 딕셔너리와 함께 DataFrame(내림차순 정렬) 을 출력하겠습니다.
sorted_tfidf_dict_df = tfidf_dict_df.sort_values(by = 'TF-IDF', ascending = False)

sorted_tfidf_dict_df.head(20)
{'가격': 6.393557952420021, '가을': 16.236629025515136, '감사': 0.8625000538908988, '감상': 3.637673027470213, '강릉': 23.091200336040632, '강원도': 34.8302061940295, '개인': 5.435780188055476, '거리': 4.981992108923736, '걱정': 3.487447940266602, '건물': 6.281116926238548, '검색': 3.9923613836862337, '게스트': 2.9642024355858285, '겨울': 16.497252947389136, '경주': 33.35256546352177, '경치': 3.3229631953308427, '경험': 0.859259102724697, '계곡': 4.103893970239806, '계단': 2.832964683604007, '계절': 1.973998514691016, '계획': 6.794451133181978, '고기': 5.692721544645315, '고민': 3.1128601733762062, '공간': 4.50193391666527, '공원': 5.949891958555836, '공항': 1.1954082027910897, '관광': 5.690997062752237, '관광객': 1.8040961270028135, '관광지': 7.240595596083571, '관람': 4.440821389250246, '관심': 2.613581945336701, '구경': 11.564076041903746, '구매': 2.59954658853196, '구입': 2.7971695925238063, '국내': 70.12079511992081, '군산': 14.082539465795795, '규모': 0.7652835437871521, '그림': 3.7025540958203083, '근처': 13.31294738346933, '기대': 3.328957867114489, '기분': 13.808578369046922, '기억': 1.6643298954858428, '기차': 7.232726933680681, '나라': 1.2695180175326608, '나름': 37.51147655956161, '나무': 11.551886189285218, '낚시': 4.480784509372583, '날씨': 11.936331177938802, '남자': 2.508811671403011, '내부': 5.629889450930314, '내일': 4.416068741678979, '네이버': 19.00116252101422, '녹원': 0.0, '놀이': 1.7862394135675528, '다리': 7.346890415084675, '다양': 0.0, '단양': 10.39069254818721, '단풍': 4.256460724146303, '담양': 12.624198091864372, '당일치기': 12.946903275662702, '대구': 14.381399299720117, '대전': 7.219765163593271, '대표': 3.8720673856960297, '덕분': 0.0, '도시': 5.440005171568493, '동네': 7.496064626944641, '동해': 4.736416348266127, '드라마': 9.8949934837295, '리조트': 6.206512202977146, '마무리': 11.570718075307084, '마을': 13.497682823901439, '마지막': 24.091229552612024, '만족': 2.31706852651505, '맛집': 21.995815502583163, '매력': 1.7297805226190701, '맥주': 8.585773001830647, '메뉴': 5.849762943280484, '명소': 6.754772096577969, '목장': 1.6684058538297268, '무료': 8.587019563141656, '문화': 3.1123848852309925, '바다': 27.67800183411619, '바람': 7.164166284257318, '바위': 3.816811496254641, '바퀴': 3.3572198319085755, '박물관': 9.647093883342656, '방문': 4.404941224685342, '방법': 3.4664581576866174, '버스': 16.405976853580206, '벚꽃': 7.910946222072994, '벽화': 2.3677614153500914, '부산': 27.91430437380888, '분위기': 12.39583262583415, '블로그': 14.297680201547736, '사람': 17.106351898969493, '사랑': 3.4071399914789486, '사용': 3.14808126850183, '사이': 4.839444465036502, '사장': 0.07738232325341368, '사진': 91.94863439475053, '사찰': 5.241368004114779, '산책': 9.657810032359423, '새벽': 4.66366841515755, '생각': 10.419496460308967, '서울': 28.555330299925735, '선택': 3.205712366789758, '설명': 1.6751111590821215, '세계': 3.6754431605144773, '세상': 7.601936332785268, '세트': 1.167699490407507, '소개': 7.046788275878054, '소리': 3.5971202972386025, '소원': 2.8704520740204154, '속초': 15.096018409673398, '수목원': 3.247229698076225, '수영장': 6.789239135188094, '숙박': 6.414199512569173, '숙소': 17.38850610135068, '순간': 9.169226000315536, '순천': 11.142336638412088, '스카이': 1.4085221996242927, '스타': 0.06375767130633382, '시간': 33.04103072490416, '시기': 1.623575242556328, '시대': 0.8217541235430792, '시설': 3.00031451797214, '시작': 13.01558118457118, '시장': 3.562499926425082, '식당': 4.282284451472149, '식사': 5.168228885592762, '실내': 4.738414259692798, '아침': 22.96038895787596, '안내': 2.734334766908014, '안면': 0.0, '야경': 7.2622137864219125, '야외': 7.349155075343624, '언덕': 4.241965144445199, '여름': 20.888927037049363, '여수': 24.78192969215874, '여유': 3.2379538486904753, '여자': 17.205476742922155, '여행': 161.8247201159838, '역사': 5.0148084790669945, '연휴': 3.260983526745064, '영상': 3.0006935050202777, '영화': 7.759329862091998, '예약': 9.825665375659955, '예전': 2.9024754025961492, '오늘': 24.082493477967134, '오랜만': 0.3988454436957206, '오후': 15.11607135131701, '올해': 10.820728375351695, '요즘': 37.31814662798577, '운영': 1.6683565912122273, '운전': 3.146864921417613, '울산': 11.617625727435241, '월드': 0.8632805430751536, '유명': 4.998911637148324, '음식': 7.448051950806692, '의미': 2.01854265660924, '이날': 18.405548375723686, '이동': 11.358061755906329, '이름': 6.606977877808278, '이상': 19.217167428208125, '이야기': 10.964728507123098, '이유': 4.731213352305755, '인기': 5.796949599473475, '인생': 4.361252563996389, '인천': 6.604342336566717, '일반': 11.627024885802053, '일상': 5.000042171471045, '일정': 8.733259157732194, '일출': 6.379366027626291, '입구': 8.835310114643931, '입장': 5.629663103327495, '입장료': 9.506866873031605, '자리': 6.418608699221224, '자연': 7.290710542992404, '자전거': 6.142981454426531, '작년': 8.193287929744075, '재미': 2.797531566432068, '저녁': 15.661236930341186, '전국': 8.278338366458375, '전망대': 5.479681984327817, '전주': 23.00466090672456, '전체': 2.8774776017074397, '전화': 1.4049327383991823, '점심': 6.271472884820757, '정리': 2.581494055724935, '정보': 4.941832142736285, '정원': 2.3960839617550707, '제주': 22.08374910412931, '제주도': 39.494123612024545, '제천': 7.1202498079009215, '조식': 6.213439950781529, '주말': 11.314536680535218, '주문': 3.4370613461048434, '주변': 10.627563107789769, '주차': 5.220535473072021, '주차장': 6.456902311516032, '준비': 10.378191165575654, '중간': 3.7062912394338037, '지금': 34.03251070486761, '지역': 8.848730094046662, '진행': 1.7297122585276525, '참고': 3.714234977916353, '처음': 39.13291567560022, '체험': 7.772932727511998, '촬영': 6.470773256296756, '최고': 7.098694880923526, '추석': 4.356086346703597, '추억': 5.6201373363253335, '추천': 45.240441281436944, '축제': 9.098777555635342, '춘천': 6.45134217571424, '출발': 17.988437625098552, '출처': 13.756752296439432, '친구': 4.5949749674302405, '카메라': 9.180365099675264, '카페': 31.230989197628123, '캐리어': 11.162451255019054, '캠핑': 5.442726194957182, '커피': 15.393419046488667, '케이블카': 7.637469223791817, '코로나': 10.176582956216471, '코스': 17.119722147339672, '태안': 6.839287148925873, '택시': 3.0574126221741507, '터널': 3.26198653482869, '터미널': 2.191594506339162, '통영': 16.677376055845475, '투어': 5.076254553589229, '특별': 0.26772762254212734, '파도': 3.2805279762029453, '패스': 7.030778542361075, '펜션': 10.253819851047055, '평일': 6.0549862425674394, '평창': 8.479791648745248, '포스팅': 16.122208330669434, '포토': 0.5741906760899123, '포항': 6.387802872988463, '폭포': 2.642524906178795, '풍경': 13.296196867316343, '플레이스': 3.0994988820132505, '필요': 5.330088622781208, '하늘': 6.8951000448571245, '하루': 18.31256437123357, '하우스': 2.5426085893040193, '한옥마을': 8.896553314232555, '한잔': 12.023942683245686, '할인': 9.333120759847409, '해변': 5.716703807163716, '해수욕장': 11.52345318155189, '해운대': 7.5303514177037965, '행복': 3.3155954029127495, '호텔': 22.53764508396459, '혼자': 179.38008041127802, '홈페이지': 9.372126262980446, '화장실': 9.380186731207178, '후기': 6.640447516460986, '휴가': 7.146569151496988, '힐링': 11.783410153034893}
Out[28]:
TF-IDF
혼자 179.380080
여행 161.824720
사진 91.948634
국내 70.120795
추천 45.240441
제주도 39.494124
처음 39.132916
나름 37.511477
요즘 37.318147
강원도 34.830206
지금 34.032511
경주 33.352565
시간 33.041031
카페 31.230989
서울 28.555330
부산 27.914304
바다 27.678002
여수 24.781930
마지막 24.091230
오늘 24.082493

TF-IDF 별로 살펴보면, 혼자(178.972)가 가장 중요한 단어로 나타났습니다. 다음으로, 여행(161.106), 사진(91.873), 국내(69.72) 등의 순으로 나타났습니다. BOW 결과와 비교하면, 전체적으로 상위에 출력된 단어들은 유사했지만, 세부적인 순위에서는 차이를 보였습니다.

TF-IDF 에서는 제주도(39.373), 강원도(34.765), 경주(33.316), 서울(28.482), 부산(28.018), 여수(24.726) 등의 지역의 중요도가 높은 것으로 나타났습니다.

수집과정에서 입력한 검색어를 제외하면, 사진, 추천, 시간, 바다 등의 단어가 빈도도 높고 중요도도 높은 것으로 나타났습니다.

4. 사회연결망분석(SNA)

  • 사회연결망 분석(소셜네트워크 분석) : 분석 대상(node)들의 관계(edge)를 연결망(network) 구조로 표현하고 이를 계량적으로 제시하는 분석기법
  • 본 프로젝트에서는 빈도가 높은 300개의 단어(node)들 간의 어떠한 관계(edge)를 네트워크 지도로 살펴보겠습니다.
In [29]:
import networkx as nx
In [30]:
travel_sentences = []
for content in contents:
    travel_sentences.extend(re.split(';|\.|\?|\!', content)) # 블로그 내용에 대해서 문장으로 나누기 위해서 문장의 끝을 나타내는 
                                                             # ;,.,?,! 를 구분자로 사용하겠습니다.
travel_sentences[:10]
Out[30]:
['여행1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지',
 ' 현짱 ・ 2019',
 ' 7',
 ' 5',
 ' 15:14url 복사 이웃추가본문 기타 기능번역보기1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지',
 '안녕하세요호',
 ' 현짱입니다',
 '한때는 평화로운 쓰리현과주노빠어느 식사자리저는 항상 여행을 떠나고 싶어하는 사람이고그날 주노빠의 튼이 태어나기 전에 동해바다보고싶다',
 '한마디에 헐 그럼 우리 강원도 양양 고',
 ' 그래 call ']
In [31]:
# 블로그 내용을 문장별로 구분하고, 구분된 문장 별로 명사를 추출하여 정리하겠습니다

travel_sentence_nouns = []
for sentence in travel_sentences:
    sentence_nouns = mecab.nouns(sentence)
    if sentence_nouns not in stop_words:
        travel_sentence_nouns.append(sentence_nouns)

travel_sentence_nouns[0:5]
Out[31]:
[['여행', '박', '일', '국내', '여행', '양양', '여행', '비앤비', '플', '서피', '비치'],
 ['짱'],
 [],
 [],
 ['복사',
  '이웃',
  '추가',
  '본문',
  '기타',
  '기능',
  '번역',
  '박',
  '일',
  '국내',
  '여행',
  '양양',
  '여행',
  '비앤비',
  '플',
  '서피',
  '비치']]
In [32]:
# 상위 단어 top_nouns 에 대해서 key에 해당하는 단어, value에 해당하는 id를 넣어 딕셔너리 형태로 저장하겠습니다.

travel_word2id = {word: num for num, word in enumerate(sorted_tfidf_dict_df.index)}
sorted(travel_word2id.items(), key=lambda x: x[1], reverse=False)[:10]
Out[32]:
[('혼자', 0),
 ('여행', 1),
 ('사진', 2),
 ('국내', 3),
 ('추천', 4),
 ('제주도', 5),
 ('처음', 6),
 ('나름', 7),
 ('요즘', 8),
 ('강원도', 9)]
In [33]:
# 상위 단어 top_nouns 에 대해서 key에 해당하는 id, value에 해당하는 단어를 넣어 딕셔너리 형태로 저장하겠습니다.

travel_id2word = {num: word for num, word in enumerate(sorted_tfidf_dict_df.index)}
sorted(travel_id2word.items(), key=lambda x: x[1], reverse=False)[:10]
Out[33]:
[(132, '가격'),
 (39, '가을'),
 (248, '감사'),
 (188, '감상'),
 (20, '강릉'),
 (9, '강원도'),
 (153, '개인'),
 (162, '거리'),
 (191, '걱정'),
 (135, '건물')]
In [34]:
# 상위 단어들에 대해서 그 개수만큼의 인접 행렬을 만들고, 문장 내에 상위 단어가 함께 포함된 비중에 따라 가중치를 계산하겠습니다.
# 행렬에서 가중치가 0 이상이면 서로 연결되어 있음을 의미합니다.

travel_adjacent_matrix = np.zeros((len(travel_word2id), len(travel_word2id)), int)
for sentence in travel_sentence_nouns:
    for wi, i in travel_word2id.items():
        if wi in sentence:
            for wj, j in travel_word2id.items():
                if i != j and wj in sentence:
                    travel_adjacent_matrix[i][j] += 1
travel_adjacent_matrix
Out[34]:
array([[  0, 777, 142, ...,   6,  14,   6],
       [777,   0, 348, ...,  15,  40,  39],
       [142, 348,   0, ...,  16,  10,  13],
       ...,
       [  6,  15,  16, ...,   0,   0,   0],
       [ 14,  40,  10, ...,   0,   0,   2],
       [  6,  39,  13, ...,   0,   2,   0]])
In [35]:
# 인접 행렬로 연결망을 만들겠습니다.

travel_network = nx.from_numpy_matrix(travel_adjacent_matrix)
list(travel_network.adjacency())[0]
Out[35]:
(0,
 {1: {'weight': 777},
  2: {'weight': 142},
  3: {'weight': 473},
  4: {'weight': 90},
  5: {'weight': 44},
  6: {'weight': 46},
  7: {'weight': 22},
  8: {'weight': 24},
  9: {'weight': 30},
  10: {'weight': 21},
  11: {'weight': 46},
  12: {'weight': 136},
  13: {'weight': 43},
  14: {'weight': 41},
  15: {'weight': 29},
  16: {'weight': 73},
  17: {'weight': 20},
  18: {'weight': 23},
  19: {'weight': 27},
  20: {'weight': 69},
  21: {'weight': 16},
  22: {'weight': 34},
  23: {'weight': 34},
  24: {'weight': 45},
  25: {'weight': 41},
  26: {'weight': 28},
  27: {'weight': 19},
  28: {'weight': 8},
  29: {'weight': 20},
  30: {'weight': 22},
  31: {'weight': 20},
  32: {'weight': 30},
  33: {'weight': 237},
  34: {'weight': 61},
  35: {'weight': 103},
  36: {'weight': 17},
  37: {'weight': 31},
  38: {'weight': 32},
  39: {'weight': 19},
  40: {'weight': 28},
  41: {'weight': 23},
  42: {'weight': 30},
  43: {'weight': 7},
  44: {'weight': 23},
  45: {'weight': 23},
  46: {'weight': 17},
  47: {'weight': 31},
  48: {'weight': 20},
  49: {'weight': 6},
  50: {'weight': 23},
  51: {'weight': 20},
  52: {'weight': 33},
  53: {'weight': 38},
  54: {'weight': 50},
  55: {'weight': 17},
  56: {'weight': 19},
  57: {'weight': 19},
  58: {'weight': 31},
  59: {'weight': 41},
  60: {'weight': 1},
  61: {'weight': 3},
  62: {'weight': 17},
  63: {'weight': 52},
  64: {'weight': 16},
  65: {'weight': 18},
  66: {'weight': 13},
  67: {'weight': 21},
  68: {'weight': 8},
  69: {'weight': 15},
  70: {'weight': 26},
  71: {'weight': 11},
  72: {'weight': 22},
  73: {'weight': 118},
  74: {'weight': 5},
  75: {'weight': 26},
  76: {'weight': 7},
  77: {'weight': 11},
  78: {'weight': 7},
  79: {'weight': 18},
  80: {'weight': 19},
  81: {'weight': 11},
  82: {'weight': 8},
  83: {'weight': 8},
  84: {'weight': 2},
  85: {'weight': 3},
  86: {'weight': 8},
  87: {'weight': 15},
  88: {'weight': 18},
  89: {'weight': 8},
  90: {'weight': 14},
  91: {'weight': 13},
  92: {'weight': 28},
  93: {'weight': 5},
  94: {'weight': 12},
  95: {'weight': 3},
  96: {'weight': 14},
  97: {'weight': 13},
  98: {'weight': 8},
  99: {'weight': 18},
  100: {'weight': 11},
  101: {'weight': 4},
  102: {'weight': 12},
  103: {'weight': 9},
  104: {'weight': 5},
  105: {'weight': 23},
  106: {'weight': 2},
  107: {'weight': 17},
  108: {'weight': 17},
  109: {'weight': 19},
  110: {'weight': 16},
  111: {'weight': 16},
  112: {'weight': 7},
  113: {'weight': 34},
  114: {'weight': 10},
  115: {'weight': 2},
  116: {'weight': 12},
  117: {'weight': 21},
  118: {'weight': 5},
  119: {'weight': 20},
  120: {'weight': 13},
  121: {'weight': 32},
  122: {'weight': 3},
  123: {'weight': 7},
  124: {'weight': 15},
  125: {'weight': 10},
  126: {'weight': 4},
  127: {'weight': 10},
  128: {'weight': 9},
  129: {'weight': 3},
  130: {'weight': 14},
  131: {'weight': 17},
  132: {'weight': 12},
  133: {'weight': 1},
  134: {'weight': 24},
  135: {'weight': 8},
  136: {'weight': 11},
  137: {'weight': 13},
  138: {'weight': 8},
  139: {'weight': 8},
  140: {'weight': 19},
  141: {'weight': 26},
  142: {'weight': 5},
  143: {'weight': 8},
  144: {'weight': 43},
  145: {'weight': 8},
  146: {'weight': 20},
  147: {'weight': 9},
  148: {'weight': 6},
  149: {'weight': 15},
  150: {'weight': 4},
  151: {'weight': 7},
  152: {'weight': 13},
  153: {'weight': 12},
  154: {'weight': 21},
  155: {'weight': 3},
  156: {'weight': 8},
  157: {'weight': 13},
  158: {'weight': 13},
  159: {'weight': 2},
  160: {'weight': 10},
  161: {'weight': 20},
  162: {'weight': 30},
  163: {'weight': 23},
  164: {'weight': 12},
  165: {'weight': 4},
  166: {'weight': 7},
  167: {'weight': 12},
  168: {'weight': 9},
  169: {'weight': 48},
  170: {'weight': 13},
  171: {'weight': 11},
  172: {'weight': 4},
  173: {'weight': 8},
  174: {'weight': 31},
  175: {'weight': 11},
  176: {'weight': 8},
  177: {'weight': 10},
  178: {'weight': 2},
  179: {'weight': 4},
  180: {'weight': 3},
  181: {'weight': 13},
  182: {'weight': 6},
  183: {'weight': 4},
  184: {'weight': 10},
  185: {'weight': 4},
  186: {'weight': 13},
  187: {'weight': 4},
  188: {'weight': 11},
  189: {'weight': 11},
  190: {'weight': 17},
  191: {'weight': 18},
  192: {'weight': 6},
  193: {'weight': 8},
  194: {'weight': 20},
  195: {'weight': 5},
  196: {'weight': 8},
  197: {'weight': 8},
  198: {'weight': 20},
  199: {'weight': 15},
  200: {'weight': 2},
  201: {'weight': 10},
  202: {'weight': 3},
  203: {'weight': 30},
  204: {'weight': 16},
  205: {'weight': 14},
  206: {'weight': 25},
  207: {'weight': 17},
  208: {'weight': 13},
  209: {'weight': 13},
  210: {'weight': 9},
  211: {'weight': 11},
  212: {'weight': 5},
  213: {'weight': 33},
  214: {'weight': 14},
  215: {'weight': 36},
  216: {'weight': 3},
  217: {'weight': 12},
  218: {'weight': 17},
  219: {'weight': 5},
  220: {'weight': 7},
  221: {'weight': 8},
  222: {'weight': 15},
  223: {'weight': 7},
  224: {'weight': 25},
  225: {'weight': 34},
  226: {'weight': 20},
  227: {'weight': 5},
  228: {'weight': 2},
  229: {'weight': 11},
  230: {'weight': 7},
  231: {'weight': 9},
  232: {'weight': 5},
  233: {'weight': 11},
  234: {'weight': 13},
  235: {'weight': 11},
  236: {'weight': 13},
  237: {'weight': 8},
  238: {'weight': 3},
  239: {'weight': 7},
  240: {'weight': 17},
  241: {'weight': 8},
  242: {'weight': 4},
  243: {'weight': 5},
  244: {'weight': 17},
  245: {'weight': 8},
  246: {'weight': 2},
  247: {'weight': 7},
  248: {'weight': 7},
  249: {'weight': 14},
  250: {'weight': 6},
  251: {'weight': 4},
  252: {'weight': 10},
  253: {'weight': 22},
  254: {'weight': 9},
  255: {'weight': 9},
  256: {'weight': 9},
  257: {'weight': 4},
  258: {'weight': 6},
  259: {'weight': 14},
  260: {'weight': 6}})

4.1 연결 중심성

  • 특정 단어에 직접 연결된 다른 단어의 수가 많으면 연결 중심성이 높다고 해석합니다.
In [36]:
# id에 대응되는 단어 컬럼을 생성하고 테이블을 만드는 함수를 생성하겠습니다.
def create_df(x):
    word_list = []
    for i in x['id']:
        word_list.append(travel_id2word.get(i))
    x['word'] = word_list
    return x
In [37]:
# 네트워크를 만들고 중심성별로 내림차순 정렬하겠습니다.
deg = nx.degree_centrality(travel_network)
sorted_deg = sorted(deg.items(), key = lambda x: x[1], reverse=True)

# 데이터프레임으로 만들겠습니다.
df_sorted_deg = pd.DataFrame(sorted_deg)
df_sorted_deg.columns = ['id', 'deg_cent']

# word 추가 함수 적용
df_sorted_deg = create_df(df_sorted_deg)
df_sorted_deg
Out[37]:
id deg_cent word
0 0 1.000000 혼자
1 1 1.000000 여행
2 2 1.000000 사진
3 3 1.000000 국내
4 12 1.000000 시간
... ... ... ...
256 257 0.476923 안면
257 216 0.461538 소원
258 258 0.434615 녹원
259 68 0.430769 캐리어
260 118 0.419231 패스

261 rows × 3 columns

4.2 위세 중심성

  • 연결된 상대 단어의 중요성에 가중치를 두어 중요한 단어(연결 중심성이 높은 다른 단어)와 많이 연결될수록 위세 중심성이 높다고 해석합니다.
In [38]:
# 네트워크를 만들고 중심성별로 내림차순 정렬하겠습니다.
eig = nx.eigenvector_centrality(travel_network, weight='weight')
sorted_eig = sorted(eig.items(), key = lambda x: x[1], reverse=True)

# 데이터프레임으로 만들겠습니다.
df_sorted_eig = pd.DataFrame(sorted_eig)
df_sorted_eig.columns = ['id', 'eigen_cent']
df_sorted_eig  # 출력하면 id에 매핑된 단어가 포함되지 않아 id에 해당하는 단어를 추가하겠습니다.

# word 추가 함수 적용
df_sorted_eig = create_df(df_sorted_eig)
df_sorted_eig
Out[38]:
id eigen_cent word
0 1 0.603552 여행
1 3 0.553467 국내
2 0 0.209561 혼자
3 4 0.173318 추천
4 12 0.135866 시간
... ... ... ...
256 104 0.006585 동네
257 83 0.006431 화장실
258 118 0.006338 패스
259 258 0.005772 녹원
260 142 0.005447 메뉴

261 rows × 3 columns

4.3 근접 중심성

  • 직간접적으로 연결된 다른 모든 단어간의 거리를 계산하여 전체 연결망의 중심적 위치에서 다른 단어들과 가까운 위치에 위치하고 있다면, 근접 중심성이 높다고 해석합니다.
In [39]:
# 네트워크를 만들고 중심성별로 내림차순 정렬하겠습니다.
clo = nx.closeness_centrality(travel_network, distance='weight')
sorted_clo = sorted(clo.items(), key = lambda x: x[1], reverse=True)

# 데이터프레임으로 만들겠습니다.
df_sorted_clo = pd.DataFrame(sorted_clo)
df_sorted_clo.columns = ['id', 'closure_cent']
df_sorted_clo  # 출력하면 id에 매핑된 단어가 포함되지 않아 id에 해당하는 단어를 추가하겠습니다.

# word 추가 함수 적용
df_sorted_clo = create_df(df_sorted_clo)
df_sorted_clo
Out[39]:
id closure_cent word
0 192 0.563991 방법
1 84 0.562771 홈페이지
2 168 0.562771 새벽
3 211 0.562771 영상
4 104 0.559140 동네
... ... ... ...
256 2 0.294785 사진
257 73 0.289210 생각
258 12 0.184922 시간
259 3 0.131446 국내
260 1 0.072504 여행

261 rows × 3 columns

4.4 매개 중심성

  • 다른 단어들 사이에 얼마나 매개자 역할을 수행하는가에 가중치를 두는 것으로 매개역할을 많을수록 매개 중심성이 높다고 해석합니다.
In [40]:
# 네트워크를 만들고 중심성별로 내림차순 정렬하겠습니다.
betw = nx.current_flow_betweenness_centrality(travel_network)
sorted_betw = sorted(betw.items(), key = lambda x: x[1], reverse=True)

# 데이터프레임으로 만들겠습니다.
df_sorted_betw = pd.DataFrame(sorted_betw)
df_sorted_betw.columns = ['id', 'between_cent']
df_sorted_betw  # 출력하면 id에 매핑된 단어가 포함되지 않아 id에 해당하는 단어를 추가하겠습니다.

# word 추가 함수 적용
df_sorted_betw = create_df(df_sorted_betw)
df_sorted_betw
Out[40]:
id between_cent word
0 3 0.006408 국내
1 1 0.006408 여행
2 12 0.006408 시간
3 0 0.006408 혼자
4 2 0.006408 사진
... ... ... ...
256 133 0.003727 포항
257 216 0.003619 소원
258 118 0.003451 패스
259 68 0.003436 캐리어
260 258 0.003425 녹원

261 rows × 3 columns

4.5 중심성(종합)

In [41]:
# 각 중심성별로 상위 10개의 단어와 중심성 지수를 정리했습니다.

total_word = pd.DataFrame(df_sorted_eig[['word', 'eigen_cent']])
total_word['연결중심성_word'] = df_sorted_deg['word']
total_word['연결중심성_cent'] = df_sorted_deg['deg_cent']

total_word['근접중심성_word'] = df_sorted_clo['word']
total_word['근접중심성_cent'] = df_sorted_clo['closure_cent']

total_word['매개중심성_word'] = df_sorted_betw['word']
total_word['매개중심성_cent'] = df_sorted_betw['between_cent']
total_word.columns = ['위세_단어', '위세중심성', '연결_단어', '연결중심성', '근접_단어','근접중심성', '매개_단어','매개중심성']
total_word.head(15)
Out[41]:
위세_단어 위세중심성 연결_단어 연결중심성 근접_단어 근접중심성 매개_단어 매개중심성
0 여행 0.603552 혼자 1.000000 방법 0.563991 국내 0.006408
1 국내 0.553467 여행 1.000000 홈페이지 0.562771 여행 0.006408
2 혼자 0.209561 사진 1.000000 새벽 0.562771 시간 0.006408
3 추천 0.173318 국내 1.000000 영상 0.562771 혼자 0.006408
4 시간 0.135866 시간 1.000000 동네 0.559140 사진 0.006408
5 사진 0.121120 추천 0.996154 태안 0.559140 생각 0.006393
6 생각 0.119798 사람 0.996154 규모 0.559140 사람 0.006370
7 코스 0.105725 생각 0.996154 사장 0.559140 코스 0.006367
8 사람 0.094429 바다 0.988462 플레이스 0.557940 추천 0.006361
9 여자 0.091146 코스 0.988462 중간 0.556745 날씨 0.006304
10 바다 0.076083 날씨 0.984615 구입 0.556745 방문 0.006302
11 시작 0.067760 방문 0.984615 관람 0.555556 바다 0.006297
12 강원도 0.066448 시작 0.980769 수목원 0.555556 유명 0.006260
13 서울 0.065354 유명 0.976923 경험 0.555556 시작 0.006256
14 제주도 0.065329 처음 0.965385 경치 0.554371 거리 0.006204

1. 각 중심성별로 상위 10개의 단어를 살펴보면, 국내, 여행, 시간, 사진, 사람, 추천, 생각 등이 각 중심성에서 공통적으로 중요도가 높은 것으로 나타났습니다.

2. 위세 중심성을 살펴보면, 강원도, 서울, 제주도 등의 지역 관련 단어가 다른 중심성에 비해 중요도가 높게 나타났습니다.

3. 매개 중심성을 살펴보면, 다른 중심성에 비해 값이 작은 것으로 보아 단어 간의 직접 연결이 많다는 것을 알 수 있습니다.

In [42]:
# 각 중심성별로 상위 10개의 단어를 시각화하겠습니다.

fig, ax = plt.subplots(2,2, figsize = (15,10))

sns.barplot(data = df_sorted_eig.head(10), x = 'word', y= 'eigen_cent', ax = ax[0][0])
sns.barplot(data = df_sorted_deg.head(10), x = 'word', y= 'deg_cent', ax = ax[0][1])
sns.barplot(data = df_sorted_clo.head(10), x = 'word', y= 'closure_cent', ax = ax[1][0])
sns.barplot(data = df_sorted_betw.head(10), x = 'word', y= 'between_cent', ax = ax[1][1])

ax[0][0].set_title('위세 중심성')
ax[0][1].set_title('연결 중심성')
ax[1][0].set_title('근접 중심성')
ax[1][1].set_title('매개 중심성')
plt.subplots_adjust(hspace = 0.3)

4.6 네트워크 그림

  • networkx에서 기본적으로 제공하는 시각화 레이아웃 중 spring layout을 사용하겠습니다.
  • node size는 위세 중심성을 기초로 설정했습니다.
In [43]:
# 생성된 연결망 데이터를 시각화하겠습니다.

import matplotlib.pyplot as plt

plt.figure(figsize=(50, 40))

nx.draw_spring(travel_network, labels = travel_id2word, font_family = 'Malgun Gothic', font_color='blue',
               node_color = ['yellow'], node_size = [v * 20000 for _,v in list(sorted_eig)], font_size=25, font_weight='bold',
              edge_color = '#D4D5CE')
plt.title('Spring Layout', fontsize = 50)
Out[43]:
Text(0.5, 1.0, 'Spring Layout')
  • 위세 중심성은 연결된 단어의 연결성까지도 계산함으로 연결된 단어가 다른 단어와 연결성이 높다면, 해당 단어의 위세 중심성은 높다고 판단하는 중심성이며, 즉 영향력이 높은지를 측정합니다.
  • 결과는 아래와 같습니다.
- 네트워크 지도에서 노드 크기가 큰 단어를 살펴보면, 여행, 국내, 혼자, 추천, 사진, 제주도(제주), 시간, 카페 등으로 나타났으며, 네트워크 상에서 중심부에 위치하고 있습니다.

- 네트워크 지도에 포함된 지역을 살펴보면, 제주, 강원도(강릉, 춘천, 속초, 평창-대관령, 동해), 부산(해운대), 인천, 태안, 군산, 여수, 울산, 통영, 포항 등 바닷가 인접 지역이 많이 포함되었습니다. 이외, 대구, 서울, 순천, 경주, 전주(한옥마을), 단양, 제천, 대전 등 대도시를 포함한 다양한 도시들도 포함되었습니다. 결과를 종합하면, '바다'라는 키워드를 중심으로 사람들은 항구도시를 여행지로 선호하는 것으로 보였으며, 그 외 대구, 서울, 경주, 전주 등 대도시 및 옛 전통이 깊은 도시도 많은 사람이 찾는 것으로 보여집니다.

- 여행에서의 계획을 짤 때 고려하는 사항은 맛집, 카페, 숙소, 거리, 계절, 날씨 등의 중요도가 높은 것으로 나타났으며, 그 중에서도 맛집, 카페 등 먹거리에 대한 관심이 높은 것으로 보여졌습니다.

4.7 커뮤니티 탐지

  • 위의 네트워크 지도에서는 커뮤니티를 찾기 힘듭니다. 아래에서 단어들 간의 커뮤니티를 찾아 그래프로 출력해보겠습니다.

  • 커뮤니티를 찾기 위해 Louvain 알고리즘을 사용하겠습니다. 이 알고리즘은 다른 알고리즘과는 달리 계산 시간이 빠르다는 장점으로 많이 이용되고 있습니다.

In [44]:
# 커뮤니티를 찾기 위한 modularity 를 계산하겠습니다.

from community import community_louvain

# 노드 속성을 기초로 파티션을 나누겠습니다.
partition = community_louvain.best_partition(travel_network)

# partition 값을 통해 노드가 잘 구분되어 있는지를 계산하겠습니다.
modularity = community_louvain.modularity(partition, travel_network)
print('Modularity:', modularity)
Modularity: 0.10371374587395288
In [45]:
plt.figure(figsize=(25, 20))

colors = [partition[n] for n in travel_network.nodes()]
my_colors = plt.cm.Set3_r
nx.draw(travel_network, with_labels = True, labels = travel_id2word, font_family = 'Malgun Gothic', 
        node_color=colors, cmap = my_colors, font_size = 14, edge_color = "#D4D5CE")

커뮤니티 탐지 결과, 5개의 커뮤니티로 나누어졌습니다. 각 커뮤니티의 노드 색상은 노랑, 하늘, 회색, 보라, 분홍색으로 출력되었습니다.

현재 생성된 커뮤니티 갯수가 최적인지를 확인하기 위해 아래에서 엘보우 기법을 활용하겠습니다.

In [46]:
from sklearn.cluster import KMeans  
from gensim.models import Word2Vec  # 언어의 의미와 유사도를 고려하여 언어를 벡터로 매핑하는 방식을 사용하는 패키지
from sklearn.manifold import TSNE   # t-SNE는 원본 데이터를 가장 잘 표현할 수 있도록 데이터의 차원을 줄이는 알고리즘
In [47]:
# 데이터

travel_nouns = [list(vocab_dict.keys())]

travel_nouns[0][:10]
Out[47]:
['여행', '국내', '사진', '시간', '생각', '사람', '혼자', '추천', '바다', '코스']
In [48]:
# 단어 임베딩을 실시하여 벡터를 구하겠습니다.

travel_word2vec = Word2Vec(travel_nouns, min_count= 1)
In [49]:
# 단어벡터에 대한 유사도를 구하겠습니다.
travel_vocab = travel_word2vec.wv.vocab
travel_similarity = travel_word2vec[travel_vocab]
<ipython-input-49-5642733a32f0>:3: DeprecationWarning: Call to deprecated `__getitem__` (Method will be removed in 4.0.0, use self.wv.__getitem__() instead).
  travel_similarity = travel_word2vec[travel_vocab]
In [50]:
# TSNE 라이브러리를 활용하여 특징 선택 방법을 통한 차원 축소를 실시하겠습니다.

travel_tsne = TSNE(n_components=2)  # n_components=2 == 2차원으로 변환하겠습니다.
In [51]:
# 위에서 차원축소한 데이터를 학습시키고 변환시키겠습니다(fit_transform()메서드).

travel_transform_similarity = travel_tsne.fit_transform(travel_similarity)
travel_df = pd.DataFrame(travel_transform_similarity, index = travel_vocab, columns=['x', 'y'])

travel_df.head()
Out[51]:
x y
여행 5.969330 6.038617
국내 -7.803099 5.821302
사진 -0.046423 -2.585906
시간 4.000901 -0.413756
생각 -2.405668 4.484372
  • K-means 클러스터링은 비지도 학습 알고리즘으로서 클러스터 내 오차제곱합(SSE)의 값이 최소가 되도록 클러스터의 중심을 결정해나가는 방법입니다. elbow 방법은 클러스터 개수에 따른 데이터의 SSE값을 그래프로 그려 최적의 클러스터 개수를 발견하는 방법입니다.
In [52]:
# 엘보우 기법을 통해 적절한 클러스터 개수를 구하겠습니다.

def elbow(x):
    distortions  = []
    for i in range(1,20):
        km = KMeans(n_clusters = i, init='k-means++', random_state = 33)
        km.fit(x)
        distortions.append(km.inertia_)  # inertia_ : kmeans 클러스터링으로 계산된 SSE 값
    
    plt.plot(range(1,20), distortions, marker='o')
    plt.plot([6,6],[6,13000], ':')
    ticks = plt.xticks(range(len(distortions)+1))
    plt.xlabel('클러스터 갯수')
    plt.ylabel('SSE')
    plt.show()
    
elbow(travel_df)

엘보우 기법을 통해 최적의 클러스터 개수는 6에서 기울기가 작아지는 것을 확인했습니다. 아래에서 클러스터의 수를 6으로 설정하여 군집분석을 수행하겠습니다.

5. 군집분석

  • 위에서 찾은 클러스터 개수를 활용하여 군집분석을 하기 위해 k-means 알고리즘을 사용하겠습니다.
  • 군집분석은 데이터의 어떤 영역을 대표하는 클러스터 중심을 찾습니다. 먼저, 데이터 포인트를 가장 가까운 클러스터 중심에 할당하고, 그런 다음 클러스터에 할당된 데이터 포인트의 평균으로 클러스터 중심을 다시 지정하는 단계를 거치는 방법입니다.
In [53]:
# 엘보우 기법으로 찾은 클러스터 갯수로 군집분석을 실시하겠습니다.

travel_kmeans = KMeans(n_clusters = 6)
travel_predict = travel_kmeans.fit_predict(travel_df)
travel_predict
Out[53]:
array([1, 0, 5, 1, 3, 3, 5, 4, 0, 4, 1, 5, 2, 0, 3, 2, 2, 4, 4, 2, 4, 3,
       0, 5, 3, 3, 2, 5, 4, 1, 5, 4, 0, 2, 2, 2, 3, 3, 0, 3, 5, 5, 5, 3,
       3, 5, 3, 2, 2, 2, 5, 3, 1, 1, 1, 2, 3, 0, 5, 5, 3, 1, 4, 4, 5, 4,
       5, 3, 1, 2, 1, 1, 4, 4, 3, 1, 2, 2, 4, 0, 1, 1, 2, 5, 5, 0, 5, 3,
       5, 5, 4, 5, 4, 0, 5, 4, 2, 0, 5, 5, 3, 5, 1, 0, 5, 4, 4, 2, 0, 0,
       5, 4, 5, 5, 1, 5, 1, 1, 5, 2, 4, 2, 2, 1, 4, 3, 4, 3, 1, 5, 3, 1,
       3, 2, 3, 4, 3, 0, 3, 1, 5, 1, 4, 3, 2, 3, 1, 0, 3, 5, 1, 2, 5, 1,
       5, 1, 3, 0, 3, 0, 1, 3, 1, 1, 1, 3, 1, 5, 2, 1, 5, 2, 2, 4, 4, 5,
       0, 2, 2, 2, 0, 1, 3, 2, 0, 1, 3, 1, 0, 5, 2, 0, 4, 0, 2, 0, 5, 3,
       3, 0, 4, 3, 2, 1, 5, 3, 0, 4, 1, 2, 2, 4, 5, 1, 4, 4, 1, 4, 4, 3,
       4, 5, 3, 2, 3, 5, 2, 3, 4, 4, 2, 1, 3, 0, 1, 2, 1, 4, 0, 1, 0, 4,
       0, 5, 2, 4, 1, 0, 4, 0, 0, 5, 3, 0, 0, 3, 3, 5, 5, 2, 0])
In [54]:
travel_results = travel_df
travel_results['cluster'] = travel_predict
travel_results
Out[54]:
x y cluster
여행 5.969330 6.038617 1
국내 -7.803099 5.821302 0
사진 -0.046423 -2.585906 5
시간 4.000901 -0.413756 1
생각 -2.405668 4.484372 3
... ... ... ...
녹원 1.699391 9.286336 3
일반 1.723173 -1.967958 5
구입 -2.089574 -1.903733 5
남자 -5.975568 -1.578914 2
영상 -5.648965 0.390932 0

261 rows × 3 columns

In [55]:
# 클러스터별(0~5)로 묶인 단어 20개를 출력하는 함수를 만들겠습니다.
# 0번 클러스터에 속한 단어를 확인하겠습니다.

def cluster_output(data, cluster):
    return data[data['cluster'] == cluster]

cluster_output(travel_results, 0).head(20)
Out[55]:
x y cluster
국내 -7.803099 5.821302 0
바다 -6.073950 2.216973 0
공원 -4.658981 4.268764 0
날씨 -9.263393 1.686446 0
준비 -5.364696 1.126775 0
처음 -7.769484 1.720022 0
커피 -5.514007 6.518546 0
이상 -5.336740 6.035587 0
속초 -4.578966 6.524179 0
박물관 -3.146302 2.247799 0
캠핑 -3.614079 1.814571 0
게스트 -8.492014 0.043419 0
음식 -4.164927 4.519851 0
도시 -6.680281 4.386712 0
터미널 -8.314846 5.339489 0
오랜만 -2.959889 3.184377 0
중간 -3.427448 3.445600 0
평창 -5.540094 5.843443 0
감사 -2.898455 2.720672 0
드라마 -5.893229 0.664434 0
  • 새로이 커뮤니티 탐지를 수행하기 위해 각 단어와 클러스터 숫자를 매핑한 딕셔너리를 만들겠습니다.
In [56]:
# 각 클러스터에 포함된 단어를 저장하겠습니다.

zero_cluster = cluster_output(travel_results, 0).index
one_cluster = cluster_output(travel_results, 1).index
two_cluster = cluster_output(travel_results, 2).index
three_cluster = cluster_output(travel_results, 3).index
four_cluster = cluster_output(travel_results, 4).index
five_cluster = cluster_output(travel_results, 5).index
In [57]:
# 원본 딕셔너리를 travel_word2id 정보가 훼손될 수 있으므로 복사본인 새로운 딕셔너리를 만들겠습니다.

travel_word2id_cluster6 = travel_word2id.copy()
In [58]:
# For 문을 통해 각 클러스터에 포함된 단어와 travel_word2id_cluster6의 키 값이 일치하면 클러스터 숫자를 value 값으로 넣겠습니다. 

for word, idx in travel_word2id_cluster6.items():
    for zero in list(zero_cluster):
        if zero == word:
            travel_word2id_cluster6[word] = 0
    
    for one in list(one_cluster):
        if one == word:
            travel_word2id_cluster6[word] = 1
            
    for two in list(two_cluster):
        if two == word:
            travel_word2id_cluster6[word] = 2
            
    for three in list(three_cluster):
        if three == word:
            travel_word2id_cluster6[word] = 3
            
    for four in list(four_cluster):
        if four == word:
            travel_word2id_cluster6[word] = 4        
            
    for five in list(five_cluster):
        if five == word:
            travel_word2id_cluster6[word] = 5

print(len(travel_word2id_cluster6))
sorted(travel_word2id_cluster6.items(), key=lambda x: x[0], reverse=False)[:10]
261
Out[58]:
[('가격', 4),
 ('가을', 3),
 ('감사', 0),
 ('감상', 2),
 ('강릉', 3),
 ('강원도', 5),
 ('개인', 1),
 ('거리', 2),
 ('걱정', 2),
 ('건물', 5)]
In [59]:
# key 에 저장된 단어를 숫자로 변환하기 위해 enumerate() 함수를 사용하겠습니다.
new_travel_word2id_cluster6 = {}
for key, value in enumerate(travel_word2id_cluster6.values()):
    new_travel_word2id_cluster6[key] = value

print(len(new_travel_word2id_cluster6))
sorted(new_travel_word2id_cluster6.items(), key=lambda x: x[0], reverse=False)[:10]
261
Out[59]:
[(0, 5),
 (1, 1),
 (2, 5),
 (3, 0),
 (4, 4),
 (5, 1),
 (6, 0),
 (7, 3),
 (8, 1),
 (9, 5)]

변환된 딕셔너리 결과는 위의 결과와 일치하는 것을 확인할 수 있습니다.

In [60]:
# 군집분석 결과를 시각화하겠습니다.

plt.figure(figsize = (12, 8))
plt.rc('axes', unicode_minus = False)
sns.scatterplot(x = 'x', y = 'y', data = travel_results, hue = 'cluster', palette='deep')
plt.legend(loc = 'best')
Out[60]:
<matplotlib.legend.Legend at 0x1d74585b4f0>

아래에서 커뮤니티를 출력하겠습니다.

In [61]:
plt.figure(figsize=(25, 20))

colors = [new_travel_word2id_cluster6[n] for n in travel_network.nodes()]
my_colors = plt.cm.Set2_r
nx.draw(travel_network, with_labels = True, labels = travel_id2word, font_family = 'Malgun Gothic', 
        node_color=colors, cmap = my_colors, font_size = 14, edge_color = '#D4D5CE', font_weight='bold')

클러스터 갯수에 맞게 각 클러스터별로 연두, 분홍, 파랑, 주황, 베이지, 회색으로 출력되었습니다.

각 단어별 x, y좌표에 위치한 단어를 그래프로 출력하겠습니다.

In [62]:
travel_tsne = TSNE(n_components=2)
travel_transform_similarity = travel_tsne.fit_transform(travel_similarity)
travel_df = pd.DataFrame(travel_transform_similarity, index = travel_vocab, columns=['x', 'y'])

travel_df.head()
Out[62]:
x y
여행 -2.681141 8.468909
국내 -9.974128 2.060520
사진 -1.590345 -1.937966
시간 -5.529241 4.068483
생각 -6.212242 1.936755
In [63]:
plt.figure(figsize = (40, 20))
ax = sns.scatterplot(travel_df["x"], travel_df["y"])

for word, pos in travel_df.iterrows():
    ax.annotate(word, pos)
plt.show()

6. 유사도

6.1 문서 간 유사도

In [64]:
# TF-IDF 값을 활용하여 문서 간 코사인 유사도를 구하겠습니다.

from sklearn.metrics.pairwise import linear_kernel  # linear_kernel는 두 벡터의 dot product 를 계산해주는 라이브러리입니다.

cosine_sim = linear_kernel(df_tfidv)

cosine_sim_df = pd.DataFrame(cosine_sim)

cosine_sim_df
Out[64]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ... 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981
0 1.000000 0.189394 0.238366 0.034643 0.219803 0.101447 0.008954 0.111221 0.087650 0.088871 0.308407 0.095546 0.137145 0.146006 0.012434 0.055048 0.250739 0.170941 0.228575 0.142877 0.173402 0.229014 0.168982 0.290085 0.075378 ... 0.195837 0.123091 0.222681 0.123091 0.053317 0.375534 0.021845 0.000000 0.087039 0.205331 0.131590 0.275241 0.123091 0.107958 0.201456 0.084440 0.031782 0.221565 0.092319 0.077850 0.023262 0.399275 0.205718 0.319112 0.038925
1 0.189394 1.000000 0.079455 0.012991 0.135263 0.033816 0.008954 0.029944 0.109562 0.038088 0.051401 0.011943 0.194289 0.132733 0.055954 0.027524 0.026394 0.085470 0.434293 0.000000 0.856171 0.022901 0.036210 0.061070 0.138193 ... 0.032640 0.061546 0.000000 0.143607 0.005332 0.051209 0.021845 0.018557 0.101545 0.064166 0.115142 0.082572 0.061546 0.070173 0.067152 0.063330 0.143019 0.110782 0.076932 0.019462 0.058155 0.127042 0.057144 0.046928 0.000000
2 0.238366 0.079455 1.000000 0.009083 0.106399 0.106399 0.018781 0.035892 0.076606 0.053262 0.107820 0.475993 0.095893 0.153133 0.013041 0.000000 0.027682 0.059761 0.095893 0.000000 0.102299 0.160128 0.075955 0.064051 0.105409 ... 0.022822 0.215166 0.000000 0.172133 0.106246 0.250640 0.045823 0.000000 0.091287 0.161515 0.069007 0.057735 0.000000 0.045291 0.070430 0.265684 0.066667 0.025820 0.161374 0.040825 0.073193 0.171312 0.023973 0.078750 0.000000
3 0.034643 0.012991 0.009083 1.000000 0.077317 0.144970 0.005118 0.166272 0.592862 0.014514 0.176287 0.022756 0.078393 0.121393 0.007107 0.078665 0.132009 0.141138 0.013065 0.114337 0.024779 0.104725 0.096591 0.514896 0.043086 ... 0.099504 0.046907 0.084857 0.070360 0.030476 0.058543 0.006243 0.000000 0.082920 0.014671 0.056413 0.000000 0.035180 0.043197 0.046061 0.024133 0.018167 0.126648 0.017590 0.022250 0.066484 0.041496 0.091458 0.139487 0.022250
4 0.219803 0.135263 0.106399 0.077317 1.000000 0.075472 0.399660 0.143208 0.211922 0.028335 0.213050 0.035540 0.229565 0.311051 0.041627 0.061430 0.184082 0.275538 0.076522 0.170068 0.145127 0.153337 0.242448 0.119262 0.252347 ... 0.169975 0.183147 0.124247 0.274721 0.029749 0.190485 0.036566 0.000000 0.129505 0.143208 0.256978 0.000000 0.137361 0.180710 0.254784 0.094229 0.212798 0.164833 0.206041 0.260623 0.077876 0.101264 0.204058 0.125684 0.000000
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
977 0.023262 0.058155 0.073193 0.066484 0.077876 0.233628 0.054986 0.157622 0.134568 0.000000 0.022547 0.024448 0.175466 0.101892 0.076360 0.253546 0.010131 0.087482 0.000000 0.000000 0.049917 0.000000 0.037062 0.070321 0.115728 ... 0.033408 0.062994 0.000000 0.125988 0.000000 0.104828 0.033539 0.000000 0.044544 0.000000 0.101015 0.000000 0.000000 0.066299 0.020620 0.032410 0.048795 0.377964 0.141737 0.059761 1.000000 0.027864 0.035093 0.000000 0.239046
978 0.399275 0.127042 0.171312 0.041496 0.101264 0.101264 0.032174 0.153719 0.139985 0.182490 0.272665 0.114447 0.082138 0.111294 0.417029 0.098907 0.324052 0.250259 0.136896 0.182550 0.103852 0.219455 0.202410 0.237743 0.210675 ... 0.182450 0.245737 0.311188 0.245737 0.172433 0.408930 0.052333 0.088911 0.104257 0.122975 0.236433 0.197814 0.221163 0.129315 0.289570 0.126430 0.038069 0.029488 0.073721 0.046625 0.027864 1.000000 0.273793 0.134908 0.046625
979 0.205718 0.057144 0.023973 0.091458 0.204058 0.153043 0.013507 0.309761 0.176304 0.191530 0.598193 0.036035 0.103448 0.100120 0.037516 0.166091 0.706756 0.487108 0.172414 0.431086 0.114447 0.368523 0.473432 0.391555 0.113715 ... 0.426746 0.371391 0.727860 0.123797 0.209127 0.463524 0.098867 0.055989 0.087538 0.154881 0.397033 0.000000 0.464238 0.260585 0.364698 0.031846 0.143839 0.037139 0.046424 0.000000 0.035093 0.273793 1.000000 0.226546 0.058722
980 0.319112 0.046928 0.078750 0.139487 0.125684 0.000000 0.011093 0.105994 0.072393 0.078645 0.245627 0.064118 0.000000 0.147999 0.000000 0.068199 0.277943 0.164717 0.198228 0.165210 0.067134 0.208066 0.388797 0.283727 0.000000 ... 0.269582 0.101666 0.275880 0.000000 0.066054 0.169182 0.027064 0.000000 0.503220 0.190789 0.081514 0.136399 0.152499 0.080250 0.166390 0.052307 0.039375 0.121999 0.000000 0.096449 0.000000 0.134908 0.226546 1.000000 0.000000
981 0.038925 0.000000 0.000000 0.022250 0.000000 0.086874 0.046004 0.021979 0.150117 0.032616 0.037729 0.030683 0.058722 0.034100 0.000000 0.000000 0.000000 0.048795 0.000000 0.048941 0.000000 0.039223 0.000000 0.000000 0.064550 ... 0.000000 0.000000 0.095346 0.000000 0.013697 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.103510 0.000000 0.000000 0.000000 0.079057 0.000000 0.239046 0.046625 0.058722 0.000000 1.000000

982 rows × 982 columns

In [65]:
# 가장 유사한 문서를 찾는 함수를 만들겠습니다.

def most_similar(x, index):
    x = x.loc[index].sort_values(ascending = False).head(2)
    df = pd.DataFrame(x)
    df.columns = ['유사도']

    return df.tail(1)

most_similar(cosine_sim_df, 1)
Out[65]:
유사도
619 0.857244

예시로, 1번 문서와 가장 유사한 문서는 619번 문서이고 유사도는 0.857 으로 나타났습니다.

6.2 단어 간 유사도

In [66]:
# 데이터

dataset = []
for i in han_contents:
    dataset.append(mecab.nouns(i))
dataset = [[y for y in x if len(y) >= 2] for x in dataset]
dataset = [[y for y in x if y not in stop_words] for x in dataset]

dataset[0][:5]
Out[66]:
['여행', '국내', '여행', '양양', '여행']
  • Word2Vec(data = data, size = 벡터의 차원, window = 주변단어 범위, min_count = 최소 빈도 설정, workers = 실행할 병렬 프로세스의 수, sg = 단어 예측방식(0 == CBOW, 1 == skip-gram))
In [67]:
# Word2Vec을 통해 단어 임베딩을 수행하겠습니다.
model = Word2Vec(dataset, window = 5, min_count= 2, sg = 1)
model.init_sims(replace = True)  # init_sims() : 트레이닝이 완료되면 필요없는 메모리를 unload 시킵니다.
In [68]:
print(f"대구와 부산 두 단어 간 유사도 : {model.wv.similarity('대구', '부산')}")
대구와 부산 두 단어 간 유사도 : 0.4007211923599243
In [69]:
# 유사도를 테이블로 출력하는 함수를 만들겠습니다.
def sim_table(x):
    return pd.DataFrame(model.wv.most_similar(x, topn = 100), columns = ['단어', '유사도'])

df = sim_table('해변')
df.head(10)
Out[69]:
단어 유사도
0 영진 0.847641
1 강문 0.845968
2 몽돌 0.840978
3 경포 0.827223
4 안목 0.824498
5 테우 0.808515
6 모이 0.787049
7 인구 0.783668
8 경포대 0.778023
9 월정리 0.774877
In [70]:
# 임의로 전국 20개 여행지역을 리스트로 저장하고, '해변' 유사어 중 포함여부를 측정해 봤습니다.

city_list = ['제주', '속초', '강릉', '속초', '평창', '동해', '부산', '인천', '태안', '군산', '여수','울산', '통영', '포함', 
             '대구', '서울', '순천', '경주', '전주', '단양']
df = df[df['단어'].isin(city_list)]
df
Out[70]:
단어 유사도
22 동해 0.729861
39 강릉 0.705458

전국 여행지역 20곳을 기준으로 공통되는 키워드를 찾았으나 '해변'이라는 키워드에서만 동해와 강릉이 출력되었습니다. 예측과는 달리 혼행족이 많이 다니는 여행지 간의 공통적인 요소를 찾기 힘들었습니다. 각 여행지마다 고유의 특색이 있는 세부적인 지명과 음식 이름 등이 많이 출력되었습니다.

7. 추천 시스템

  • 추천 시스템을 개발하기 위해서는 label(지역이름) 값이 있어야 합니다. 지역이름을 추출하기 위해 다음과 같은 과정을 수행했습니다.
- 먼저, 전국의 도(ex. 경기도, 경상북도 등) 리스트와 지역(예, 서울, 부산 등) 리스트를 만들어줍니다.

- 기존 컬럼 중 'Title'과 'Description' 을 합쳐 새로운 컬럼 'txt_sum' 을 만듭니다.

- 마지막으로 반복문을 통해 'txt_sum' 컬럼에 도 및 지역 리스트가 포함되는지를 통해 해당 블로그 글이 소개하는 지역을 추출했습니다.
In [71]:
city_data = pd.read_csv('etc/korea_city_list.csv')

do_list = list(set(city_data['do'].str.strip()))[1:10]
city_list = list(set(city_data['city'].str.strip()))

data['txt_sum'] = data['Title'] + data['Description']

def find_city_name():
    do_result = []
    city_result = []

    for txt in data['txt_sum']:
        do_match = []
        city_match = []

        for target in do_list:
            search_result = str(txt).find(str(target))
            if search_result != -1:
                do_match.append(target)
#                 print(target)

        for target in city_list:
            search_result = str(txt).find(str(target))
            if search_result != -1:
                city_match.append(target)
#                 print(target)

        do_result.append(','.join(do_match))
        city_result.append(','.join(city_match))

    return do_result, city_result

do_result, city_result = find_city_name()

data['do'] = do_result
data['city'] = city_result

data[data['do'] != '']
data[data['city']!= '']

data.to_csv('first_area_name.csv', encoding = 'utf8')
  • 생성된 지역 컬럼을 출력해보겠습니다.
In [72]:
data = pd.read_csv('first_area_name.csv')

print(data.shape)
data.head()
(982, 12)
Out[72]:
Unnamed: 0 Title Link Description Blogger_Name Blogger_Link Post_Date Post_Contents raw_Post_Contents txt_sum do city
0 0 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! https://blog.naver.com/love151419?Redirect=Log... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! 안녕하세요호! 현짱입... 현짱이네 https://blog.naver.com/love151419 20190705 \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비... \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지!1박2일 국내여행 - ... NaN 양양
1 1 여자 혼자 1박2일 군산 국내여행 part.2 https://blog.naver.com/kansk92?Redirect=Log&lo... 예전부터 군산 너무 가보고 싶었는데 5년만에 혼자 다녀온 국내여행도 대성공이었다 뚜... ma vie en rose. https://blog.naver.com/kansk92 20181227 \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군... \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군... 여자 혼자 1박2일 군산 국내여행 part.2예전부터 군산 너무 가보고 싶었는데 5... NaN 군산
2 2 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... https://blog.naver.com/okinawa100?Redirect=Log... 저만 혼자 들어가서 아주 예수님 코스프레 하고 나왔습니다!! 애들이 저만 바라봐서 ... 시유 :) https://blog.naver.com/okinawa100 20200712 \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑... \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑... 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... 저만 혼자 들어가서 ... NaN 평창
3 3 국내 여행용캐리어 추천, 확장되는 20인치 하드캐리어 키코... https://blog.naver.com/accentv?Redirect=Log&lo... 국내여행으로 좋고, 해외도 혼자 출장을 갈때 사용하면 좋은 부담없는 사이즈~ 캐리어... 작지만 확실한 행복 :D https://blog.naver.com/accentv 20200709 \n\n\n\n\n\n\n원더풀코리아♥\n\n\n\n\n국내 여행용캐리어 추천, ... \n\n\n\n\n\n\n원더풀코리아♥\n\n\n\n\n국내 여행용캐리어 추천, ... 국내 여행용캐리어 추천, 확장되는 20인치 하드캐리어 키코... 국내여행으로 좋고... NaN NaN
4 4 [국내여행]원산도&amp;태안 허슬러 차박 그리고 낚시 https://blog.naver.com/rkaantm?Redirect=Log&lo... 10일 캠핑&amp;낚시 이용권 암튼 저는 여름에 프랑스를 가지 못하니, 국내 여행... 완두콩의 행복한 여행 그리고 사진 https://blog.naver.com/rkaantm 20200617 \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박... \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박... [국내여행]원산도&amp;태안 허슬러 차박 그리고 낚시10일 캠핑&amp;낚시 이용... NaN 태안
  • 성공적으로 추출되었음을 확인했습니다. 이제 지역명이 도 혹은 지역컬럼이 채워진 데이터 갯수를 살펴보겠습니다.
In [73]:
data_notnull = data.loc[data['do'].notnull() | data['city'].notnull()]

print(data_notnull.shape)
data_notnull.tail()
(791, 12)
Out[73]:
Unnamed: 0 Title Link Description Blogger_Name Blogger_Link Post_Date Post_Contents raw_Post_Contents txt_sum do city
977 995 국내여행지/포항바다 추천/구룡포항 - 아름다운 날 https://blog.naver.com/koreamoringa?Redirect=L... 즐기는 여행자의 시선과는 다른 고단함이 배어 있는지 모르겠다. 국내여행지로 추천 하... 바다사랑 https://blog.naver.com/koreamoringa 20180712 \n\n\n\n\n\n\n\n\n길따라\n\n\n\n\n\n\n\n\n\n\n국내여... \n\n\n\n\n\n\n\n\n길따라\n\n\n\n\n\n\n\n\n\n\n국내여... 국내여행지/포항바다 추천/구룡포항 - 아름다운 날즐기는 여행자의 시선과는 다른 고단... NaN 포항
978 996 국내여행 아기와함께 속초여행 1박2일 도전, 가평휴게소... https://blog.naver.com/mibongss?Redirect=Log&l... 오랜만에 단둘이 장거리 여행이라 초-긴장했지만 너무 성공적이라 속으로 혼자 엄청 감... MMYY https://blog.naver.com/mibongss 20190709 \n\n\n\n\n\n\n여행\n\n\n\n\n국내여행 아기와함께 속초여행 1박2일... \n\n\n\n\n\n\n여행\n\n\n\n\n국내여행 아기와함께 속초여행 1박2일... 국내여행 아기와함께 속초여행 1박2일 도전, 가평휴게소... 오랜만에 단둘이 장거리... NaN 가평,속초
979 997 하동 쌍계사 단풍놀이 가을의추억 : 국내여행 #440 https://blog.naver.com/kimcoco1?Redirect=Log&l... 한 여행이었지만 여행지에 도착하면 각자 흩어져 여행을 하는 편이라 아빠와 아들의 국... 재빈짱의 초보사진사 https://blog.naver.com/kimcoco1 20141111 \n\n하동 쌍계사 하면 무엇이 생각나십니까? 아마도 거의 90% 이상은 봄의 벚... \n\n하동 쌍계사 하면 무엇이 생각나십니까? 아마도 거의 90% 이상은 봄의 벚... 하동 쌍계사 단풍놀이 가을의추억 : 국내여행 #440한 여행이었지만 여행지에 도착하... NaN 하동
980 998 [국내여행] 제주 3박4일 여행 1일차 https://blog.naver.com/bin010617?Redirect=Log&... 해서 오게된 여행 (숙소,렌탈,맛집 등등 모두 내가 총대매고 계획함) 티웨이 항공이... https://blog.naver.com/bin010617 20200712 \n\n\n\n\n\n\ntravel\n\n\n\n\n[국내여행] 제주 3박4일 여... \n\n\n\n\n\n\ntravel\n\n\n\n\n[국내여행] 제주 3박4일 여... [국내여행] 제주 3박4일 여행 1일차해서 오게된 여행 (숙소,렌탈,맛집 등등 모두... NaN 제주
981 999 아기랑 국내여행_하동 더로드 101(영업시간, 베이비존) https://blog.naver.com/cocobono1?Redirect=Log&... 아기와 떠나기 좋은 여행지 하동. 아름답고 여유로워서 가도, 또 가도 좋은 곳. 지... 오늘도 안녕 https://blog.naver.com/cocobono1 20191002 \n\n\n\n\n\n\n아기와 여행\n\n\n\n\n아기랑 국내여행_하동 더로드 ... \n\n\n\n\n\n\n아기와 여행\n\n\n\n\n아기랑 국내여행_하동 더로드 ... 아기랑 국내여행_하동 더로드 101(영업시간, 베이비존)아기와 떠나기 좋은 여행지 ... NaN 하동
  • 지역명이 채워진 데이터의 총 갯수는 810개입니다. 하지만, 지역명이 두 개이상으로 채워진 케이스가 보여 확인해보겠습니다.
In [74]:
data['do'].value_counts()
Out[74]:
제주도     62
전라남도     5
경상북도     5
경기도      4
경상남도     4
전라북도     2
Name: do, dtype: int64
In [75]:
data['city'].value_counts()
Out[75]:
제주          59
경주          44
부산          36
서울          30
전주          27
            ..
부산,광주        1
양양,음성        1
공주,음성        1
인천,양산        1
김해,제주,부산     1
Name: city, Length: 263, dtype: int64
In [76]:
data['city'].unique()
Out[76]:
array(['양양', '군산', '평창', nan, '태안', '함양', '인천', '단양', '남해,서울,여수',
       '담양,부여,서산', '부여', '제주,울산,경주', '속초', '동해', '전주', '제주', '부산', '창원',
       '영주', '부여,익산', '거제', '성주', '여수', '순천', '대전,공주', '가평', '군산,전주',
       '예산,강릉', '해남', '안동', '원주', '남해', '구리,부산', '청주', '진도,경주', '포천',
       '통영,거제', '제주,서울,부여,부산,익산', '춘천', '제주,서울', '공주,울산', '담양', '강릉',
       '구미', '울산', '아산', '합천,부산', '포항', '예산', '서울,고양', '대구', '서울', '목포',
       '동해,양양', '경주', '대전,강릉,순천', '김해,제주,부산', '인천,양산', '부산,부천', '보령',
       '여주', '공주', '태안,동해,양주,춘천,전주,남양주,과천,양산', '울릉,제주', '담양,여수', '울릉,서울',
       '공주,음성', '신안', '양평,하남', '정선', '서울,강릉', '서산', '담양,광주', '통영', '합천',
       '청도', '진도', '부안', '대전', '대전,대구', '삼척', '동해,속초,김포', '남해,금산',
       '속초,양양', '상주', '제천', '부산,전주,여수,순천', '보성,순천', '태안,구례', '울진',
       '군산,대전,예산,전주,여수', '제천,문경,울산,김천', '서천,공주,서울,안산,시흥', '여수,순천',
       '진주,의령,창녕', '괴산', '단양,양구', '청양', '장흥', '서울,순천', '부여,김포,전주',
       '춘천,삼척', '대구,광주', '파주', '강릉,양양,여주', '광명', '양평', '김천', '횡성',
       '부산,여수,순천', '안성', '양산', '속초,여수', '춘천,속초', '김해', '양주', '전주,진안',
       '제주,통영', '제주,서귀포', '고성', '담양,서울,보성,남원,전주,여수,순천', '이천', '용인',
       '인천,경주', '안산', '대구,경주', '동해,속초', '부산,해남', '제주,구리', '대전,서울', '영월',
       '춘천,서울', '홍성', '울릉,안산', '서천', '의왕', '제주,김포', '문경', '제주,부산',
       '목포,공주,정읍,논산,광주', '양양,음성', '충주,제천', '광명,통영', '원주,대구,홍천', '울릉',
       '동해,삼척', '옥천,통영', '밀양', '담양,보성', '남원', '전주,통영', '김해,포항,대구',
       '남해,여수,순천', '대전,공주,계룡', '보성', '부천,영월', '영양,경주', '시흥', '홍천', '산청',
       '영덕', '충주', '세종', '정읍', '제주,인제', '강릉,삼척', '단양,사천', '사천', '하동',
       '진주', '서울,창원', '담양,남해,전주,상주', '강릉,평창', '구리', '서울,양평',
       '제주,춘천,강릉,여수', '양구', '울진,강릉', '보성,순천,장흥', '영광', '담양,부여', '당진',
       '대구,통영', '부산,울산', '고양', '구리,강릉', '광양', '서울,대구', '강진', '평택', '수원',
       '대전,천안', '동해,태백,강릉', '군산,담양,부산', '담양,남해,강릉,통영,거제', '진도,통영',
       '울진,삼척', '제주,서울,강릉', '서울,부산', '임실', '안성,전주', '영양', '진도,부안',
       '서울,청송', '단양,영월', '단양,원주', '보성,해남', '오산', '창녕', '영주,안동', '진도,제주',
       '포항,대구', '태안,곡성', '포항,경주', '대전,가평,서울', '여수,여주', '진주,거제', '태안,정선',
       '논산,광주', '대전,여수', '동해,속초,구리', '제주,경주', '제주,목포', '진도,충주,청주',
       '정선,단양,영월', '합천,여주', '단양,예천', '포항,영주,안동', '정읍,부안', '정선,평창',
       '가평,논산', '아산,고창', '포항,속초', '함안', '곡성', '부천', '부산,통영', '서울,인천',
       '광주', '양주,남양주', '김포', '목포,부산,신안,거제', '군산,양평', '영동', '제주,순천',
       '동해,청주,부산,안산,대구,천안', '제주,청주', '동해,영덕', '군산,여수', '서울,울산',
       '담양,서울,보성,광주', '단양,제천,울산', '인제', '부산,광주', '서울,전주,진안', '서울,연천',
       '울산,고창', '목포,서울', '동해,제주', '대구,순천', '진안', '양주,서울,남양주', '아산,서울',
       '가평,속초'], dtype=object)

지역이 두 개이상으로 채워진 데이터 갯수를 확인했습니다. 지역이 두 개이상 채워진 케이스는 직접 블로그 내용을 비교하며 단일 지역명으로 수정하겠습니다. 또한, 여행글과 관련없거나 여러 지역이 포함된 케이스는 지우겠습니다.

수정된 지역컬럼은 다음과 같습니다.

In [77]:
area = pd.read_csv('area_name.csv')

area_notnull = area.loc[area['do'].notnull() | area['city'].notnull()]

print(area_notnull.shape)

area_notnull.head()
(743, 11)
Out[77]:
Unnamed: 0 Title Link Description Blogger_Name Blogger_Link Post_Date Post_Contents txt_sum do city
0 0 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! https://blog.naver.com/love151419?Redirect=Log... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! 안녕하세요호! 현짱입... 현짱이네 https://blog.naver.com/love151419 20190705 \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지!1박2일 국내여행 - ... NaN 양양
1 1 여자 혼자 1박2일 군산 국내여행 part.2 https://blog.naver.com/kansk92?Redirect=Log&lo... 예전부터 군산 너무 가보고 싶었는데 5년만에 혼자 다녀온 국내여행도 대성공이었다 뚜... ma vie en rose. https://blog.naver.com/kansk92 20181227 \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군... 여자 혼자 1박2일 군산 국내여행 part.2예전부터 군산 너무 가보고 싶었는데 5... NaN 군산
2 2 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... https://blog.naver.com/okinawa100?Redirect=Log... 저만 혼자 들어가서 아주 예수님 코스프레 하고 나왔습니다!! 애들이 저만 바라봐서 ... 시유 :) https://blog.naver.com/okinawa100 20200712 \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑... 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... 저만 혼자 들어가서 ... 강원도 평창
4 4 [국내여행]원산도&amp;태안 허슬러 차박 그리고 낚시 https://blog.naver.com/rkaantm?Redirect=Log&lo... 10일 캠핑&amp;낚시 이용권 암튼 저는 여름에 프랑스를 가지 못하니, 국내 여행... 완두콩의 행복한 여행 그리고 사진 https://blog.naver.com/rkaantm 20200617 \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박... [국내여행]원산도&amp;태안 허슬러 차박 그리고 낚시10일 캠핑&amp;낚시 이용... NaN 태안
5 5 여름 국내여행 추천 : 경남 함양 상림공원 https://blog.naver.com/kayak71?Redirect=Log&lo... ^^ 아침고요수목원, 제이드가든 같은 잘 꾸며진 국내 정상급 입장료 10,000원 ... Stationary Traveller https://blog.naver.com/kayak71 20200713 \n\n\n\n\n\n\n경상남도\n\n\n\n\n여름 국내여행 추천 : 경남 함양... 여름 국내여행 추천 : 경남 함양 상림공원^^ 아침고요수목원, 제이드가든 같은 잘 ... NaN 함양
  • 위의 행렬에서 인덱스가 정리되지 않았습니다. reset_index() 를 활용하여 인덱스를 정리하겠습니다.
In [78]:
area_notnull = area_notnull.reset_index()

area_notnull.head()
Out[78]:
index Unnamed: 0 Title Link Description Blogger_Name Blogger_Link Post_Date Post_Contents txt_sum do city
0 0 0 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! https://blog.naver.com/love151419?Redirect=Log... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! 안녕하세요호! 현짱입... 현짱이네 https://blog.naver.com/love151419 20190705 \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지!1박2일 국내여행 - ... NaN 양양
1 1 1 여자 혼자 1박2일 군산 국내여행 part.2 https://blog.naver.com/kansk92?Redirect=Log&lo... 예전부터 군산 너무 가보고 싶었는데 5년만에 혼자 다녀온 국내여행도 대성공이었다 뚜... ma vie en rose. https://blog.naver.com/kansk92 20181227 \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군... 여자 혼자 1박2일 군산 국내여행 part.2예전부터 군산 너무 가보고 싶었는데 5... NaN 군산
2 2 2 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... https://blog.naver.com/okinawa100?Redirect=Log... 저만 혼자 들어가서 아주 예수님 코스프레 하고 나왔습니다!! 애들이 저만 바라봐서 ... 시유 :) https://blog.naver.com/okinawa100 20200712 \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑... 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... 저만 혼자 들어가서 ... 강원도 평창
3 4 4 [국내여행]원산도&amp;태안 허슬러 차박 그리고 낚시 https://blog.naver.com/rkaantm?Redirect=Log&lo... 10일 캠핑&amp;낚시 이용권 암튼 저는 여름에 프랑스를 가지 못하니, 국내 여행... 완두콩의 행복한 여행 그리고 사진 https://blog.naver.com/rkaantm 20200617 \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박... [국내여행]원산도&amp;태안 허슬러 차박 그리고 낚시10일 캠핑&amp;낚시 이용... NaN 태안
4 5 5 여름 국내여행 추천 : 경남 함양 상림공원 https://blog.naver.com/kayak71?Redirect=Log&lo... ^^ 아침고요수목원, 제이드가든 같은 잘 꾸며진 국내 정상급 입장료 10,000원 ... Stationary Traveller https://blog.naver.com/kayak71 20200713 \n\n\n\n\n\n\n경상남도\n\n\n\n\n여름 국내여행 추천 : 경남 함양... 여름 국내여행 추천 : 경남 함양 상림공원^^ 아침고요수목원, 제이드가든 같은 잘 ... NaN 함양
  • 기존의 인덱스를 삭제하겠습니다.
In [79]:
area_notnull = area_notnull.drop(['index', 'Unnamed: 0'], axis = 1)

area_notnull.head()
Out[79]:
Title Link Description Blogger_Name Blogger_Link Post_Date Post_Contents txt_sum do city
0 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! https://blog.naver.com/love151419?Redirect=Log... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! 안녕하세요호! 현짱입... 현짱이네 https://blog.naver.com/love151419 20190705 \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지!1박2일 국내여행 - ... NaN 양양
1 여자 혼자 1박2일 군산 국내여행 part.2 https://blog.naver.com/kansk92?Redirect=Log&lo... 예전부터 군산 너무 가보고 싶었는데 5년만에 혼자 다녀온 국내여행도 대성공이었다 뚜... ma vie en rose. https://blog.naver.com/kansk92 20181227 \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군... 여자 혼자 1박2일 군산 국내여행 part.2예전부터 군산 너무 가보고 싶었는데 5... NaN 군산
2 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... https://blog.naver.com/okinawa100?Redirect=Log... 저만 혼자 들어가서 아주 예수님 코스프레 하고 나왔습니다!! 애들이 저만 바라봐서 ... 시유 :) https://blog.naver.com/okinawa100 20200712 \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑... 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... 저만 혼자 들어가서 ... 강원도 평창
3 [국내여행]원산도&amp;태안 허슬러 차박 그리고 낚시 https://blog.naver.com/rkaantm?Redirect=Log&lo... 10일 캠핑&amp;낚시 이용권 암튼 저는 여름에 프랑스를 가지 못하니, 국내 여행... 완두콩의 행복한 여행 그리고 사진 https://blog.naver.com/rkaantm 20200617 \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박... [국내여행]원산도&amp;태안 허슬러 차박 그리고 낚시10일 캠핑&amp;낚시 이용... NaN 태안
4 여름 국내여행 추천 : 경남 함양 상림공원 https://blog.naver.com/kayak71?Redirect=Log&lo... ^^ 아침고요수목원, 제이드가든 같은 잘 꾸며진 국내 정상급 입장료 10,000원 ... Stationary Traveller https://blog.naver.com/kayak71 20200713 \n\n\n\n\n\n\n경상남도\n\n\n\n\n여름 국내여행 추천 : 경남 함양... 여름 국내여행 추천 : 경남 함양 상림공원^^ 아침고요수목원, 제이드가든 같은 잘 ... NaN 함양
  • 위에서 했던 중복값 체크를 다시 하겠습니다.
In [80]:
print('원본 포스팅 글 수 :',area_notnull.shape)

area_notnull = area_notnull.drop_duplicates(['Post_Contents'], keep = 'last') # 가장 최근 내용을 남기겠습니다.

print('중복 제거 후 포스팅 글 수 :', area_notnull.shape)
원본 포스팅 글 수 : (743, 10)
중복 제거 후 포스팅 글 수 : (734, 10)
  • 지역이 두 개이상으로 된 케이스 유무를 확인하겠습니다.
In [81]:
area_notnull['do'].unique()
Out[81]:
array([nan, '강원도', '제주도', '경상남도', '전라남도', '경기도', '전라북도', '경상북도'],
      dtype=object)
In [82]:
sns.countplot(data = area_notnull, x = 'do')
Out[82]:
<matplotlib.axes._subplots.AxesSubplot at 0x1d751fb5fd0>

시각화 결과, 도 단위 중에서는 제주도와 강원도를 가장 많이 방문하는 것으로 나타났습니다.

In [83]:
area_notnull['city'].unique()
Out[83]:
array(['양양', '군산', '평창', '태안', '함양', '인천', '단양', '여수', '부여', '속초', '동해',
       '전주', '제주', '부산', '창원', '영주', '거제', '성주', '순천', '가평', '강릉', '해남',
       '안동', '원주', '남해', '청주', nan, '경주', '포천', '춘천', '울산', '담양', '구미',
       '아산', '포항', '예산', '대구', '서울', '목포', '보령', '여주', '공주', '울릉', '신안',
       '정선', '서산', '통영', '합천', '청도', '진도', '부안', '대전', '삼척', '상주', '제천',
       '울진', '김천', '광주', '괴산', '청양', '장흥', '파주', '광명', '양평', '횡성', '안성',
       '양산', '김해', '양주', '진안', '고성', '이천', '용인', '안산', '서귀포', '영월', '홍성',
       '서천', '의왕', '문경', '홍천', '밀양', '남원', '보성', '시흥', '산청', '영덕', '충주',
       '세종', '정읍', '사천', '하동', '진주', '구리', '양구', '영광', '당진', '고양', '광양',
       '강진', '평택', '수원', '태백', '임실', '영양', '청송', '오산', '창녕', '곡성', '고창',
       '함안', '부천', '남양주', '김포', '영동', '인제', '연천'], dtype=object)
In [84]:
popular_area = area_notnull['city'].value_counts().head(10).index  # 도시 중 가장 빈도가 높은 10개

popular_city = area_notnull[area_notnull['city'].isin(popular_area)]  # 상위 10개 도시의 데이터 추출

sns.countplot(data = popular_city, x = 'city')  # 시각화
Out[84]:
<matplotlib.axes._subplots.AxesSubplot at 0x1d752093940>

빈도가 높은 상위 10개의 지역을 시각화한 결과, 제주를 가장 많이 여행하는 것으로 나타났습니다. 다음으로 경주, 부산 등의 순이었습니다.

  • 단일 지역으로 변경되었음을 확인했습니다. 이후, 'do' 컬럼과 'city' 컬럼을 합쳐 'area' 컬럼을 생성하겠습니다. 이때, 두 컬럼 모두 값이 채워졌을 경우에는 city 컬럼 데이터가 보다 구체적인 지역명이므로 이를 사용하겠습니다.
In [85]:
area_notnull['area'] = area_notnull['city']
area_notnull.loc[area_notnull['area'].isnull(), 'area'] = area_notnull['do']
In [86]:
print('결측치 수 : ',area_notnull['area'].isnull().sum())

print('최종 데이터 수 :', area_notnull.shape)
area_notnull['area'].head()
결측치 수 :  0
최종 데이터 수 : (734, 11)
Out[86]:
0    양양
1    군산
2    평창
3    태안
4    함양
Name: area, dtype: object
  • area 컬럼을 모두 정리했습니다. 총 734개 데이터를 활용하여 여행지 추천시스템을 구현하겠습니다.
  • 추천시스템 구현을 위해 feature은 Post_Contets 컬럼이며, label은 area 컬럼을 활용하겠습니다. 먼저, Post_Contents 컬럼을 개행문자 제거와 정규표현식을 이용하여 한글을 제외하고 모두 제거하겠습니다.
In [87]:
# 'Post_Contents' 를 한글을 제외한 모든 문자를 제거하하겠습니다.

def revised_text(data):
    
    contents = []
    posts = data['Post_Contents']
    for post in posts:
        post = str(post).replace('\n','').replace('\u200b','').replace('\xa0','').replace('\t','')
        post = re.sub('[^ㄱ-ㅎㅏ-ㅣ가-힣 ]',' ',post).lstrip()
        contents.append(str(post))
    return contents

area_notnull['reviesed_contents']  = revised_text(area_notnull)
In [88]:
area_notnull.head()
Out[88]:
Title Link Description Blogger_Name Blogger_Link Post_Date Post_Contents txt_sum do city area reviesed_contents
0 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! https://blog.naver.com/love151419?Redirect=Log... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지! 안녕하세요호! 현짱입... 현짱이네 https://blog.naver.com/love151419 20190705 \n\n\n\n\n\n\n여행\n\n\n\n\n1박2일 국내여행 - 양양여행 에어비... 1박2일 국내여행 - 양양여행 에어비앤비, 핫플 서피비치까지!1박2일 국내여행 - ... NaN 양양 양양 여행 박 일 국내여행 양양여행 에어비앤비 핫플 서피비치까지 현짱 ...
1 여자 혼자 1박2일 군산 국내여행 part.2 https://blog.naver.com/kansk92?Redirect=Log&lo... 예전부터 군산 너무 가보고 싶었는데 5년만에 혼자 다녀온 국내여행도 대성공이었다 뚜... ma vie en rose. https://blog.naver.com/kansk92 20181227 \n\n\n\n\n\n\ndaily life\n\n\n\n\n여자 혼자 1박2일 군... 여자 혼자 1박2일 군산 국내여행 part.2예전부터 군산 너무 가보고 싶었는데 5... NaN 군산 군산 여자 혼자 박 일 군산 국내여행 칸쵸 ...
2 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... https://blog.naver.com/okinawa100?Redirect=Log... 저만 혼자 들어가서 아주 예수님 코스프레 하고 나왔습니다!! 애들이 저만 바라봐서 ... 시유 :) https://blog.naver.com/okinawa100 20200712 \n\n\n\n\n\n\n일상\n\n\n\n\n시오양의 일상/[강원도 평창] 아이랑... 시오양의 일상/[강원도 평창] 아이랑 국내여행은 '대관령... 저만 혼자 들어가서 ... 강원도 평창 평창 일상시오양의 일상 강원도 평창 아이랑 국내여행은 대관령 하늘목장 시오 ...
3 [국내여행]원산도&amp;태안 허슬러 차박 그리고 낚시 https://blog.naver.com/rkaantm?Redirect=Log&lo... 10일 캠핑&amp;낚시 이용권 암튼 저는 여름에 프랑스를 가지 못하니, 국내 여행... 완두콩의 행복한 여행 그리고 사진 https://blog.naver.com/rkaantm 20200617 \n\n\n\n\n\n\n우리땅\n\n\n\n\n[국내여행]원산도&태안 허슬러 차박... [국내여행]원산도&amp;태안 허슬러 차박 그리고 낚시10일 캠핑&amp;낚시 이용... NaN 태안 태안 우리땅 국내여행 원산도 태안 허슬러 차박 그리고 낚시 완두콩 ...
4 여름 국내여행 추천 : 경남 함양 상림공원 https://blog.naver.com/kayak71?Redirect=Log&lo... ^^ 아침고요수목원, 제이드가든 같은 잘 꾸며진 국내 정상급 입장료 10,000원 ... Stationary Traveller https://blog.naver.com/kayak71 20200713 \n\n\n\n\n\n\n경상남도\n\n\n\n\n여름 국내여행 추천 : 경남 함양... 여름 국내여행 추천 : 경남 함양 상림공원^^ 아침고요수목원, 제이드가든 같은 잘 ... NaN 함양 함양 경상남도여름 국내여행 추천 경남 함양 상림공원 힌클랑 ...
  • 성공적으로 데이터가 변환되었습니다. 이제부터 TFIDF를 통해 코사인 유사도를 계산하여 추천시스템을 구현하겠습니다.
In [89]:
from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer(stop_words = stop_words)

# 네트워크 분석 과정과는 달리 입력 형식이 다양하기 때문에 품사에 구분없이 모두 벡터화했습니다.
tfidf_matrix = tfidf.fit_transform(area_notnull['reviesed_contents'])

print(tfidf_matrix.shape)
(734, 114450)

총 734개의 문서의 텍스트를 벡터화한 결과 114450개로 나타났습니다.

In [90]:
from sklearn.metrics.pairwise import linear_kernel

cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)  # 코사인 유사도 계산
In [91]:
print(tfidf.transform(area_notnull['reviesed_contents']).toarray())
print(tfidf)
[[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 ...
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]]
TfidfVectorizer(stop_words=['이웃', '추가', '환영', '댓글', '공감', '본문', '기타', '복사',
                            '기능', '번역', '이웃추가본문', '박일', '기타', '복사', '기능지도로',
                            '기능번역보기', '지도닫기', '전체지도', '번역보기', '몸', '뭐', '한국',
                            '빠', '픽', '힙', '동안', '가능', '여행지', '모습', '어디', ...])
In [92]:
df_tfidv = pd.DataFrame(tfidf.transform(area_notnull['reviesed_contents']).toarray())

df_tfidv # 문서(블로그) * 단어
Out[92]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ... 114425 114426 114427 114428 114429 114430 114431 114432 114433 114434 114435 114436 114437 114438 114439 114440 114441 114442 114443 114444 114445 114446 114447 114448 114449
0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.044129 0.0 0.0 0.0 0.0 0.0
1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.019218 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
729 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.032732 0.0 0.028297 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
730 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
731 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
732 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
733 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0

734 rows × 114450 columns

In [93]:
# 인덱스를 지역이름으로 저장하겠습니다.
area_list = list(area_notnull['area'].values)

df_tfidv.index = area_list

df_tfidv
Out[93]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ... 114425 114426 114427 114428 114429 114430 114431 114432 114433 114434 114435 114436 114437 114438 114439 114440 114441 114442 114443 114444 114445 114446 114447 114448 114449
양양 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.044129 0.0 0.0 0.0 0.0 0.0
군산 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
평창 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
태안 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
함양 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.019218 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
포항 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.032732 0.0 0.028297 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
속초 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
하동 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
제주 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
하동 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0

734 rows × 114450 columns

In [94]:
word_tfidv = tfidf.vocabulary_   # 벡터 단어 출력

sort_word = sorted(word_tfidv.items())

sort_word_list = []
for i in sort_word:
    sort_word_list.append(i[0])

print(len(sort_word_list))  # 한 글자 단어를 제외한 총 261개의 단어가 리스트로 만들어졌습니다.
sort_word_list[:10]
114450
Out[94]:
['ㄱㄱ',
 'ㄱㄱㄱㄱ멀리서보는게',
 'ㄱ그글그렇',
 'ㄱ하자ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ그렇게',
 'ㄴ자형으로',
 'ㄷㄷ',
 'ㄷㄷㄷ',
 'ㄷㄷㄷㄷ',
 'ㄷㄷ나처럼',
 'ㄷㄷ다른반']
In [95]:
# 벡터 단어를 컬럼으로 저장하겠습니다.
df_tfidv.columns = sort_word_list

df_tfidv
Out[95]:
ㄱㄱ ㄱㄱㄱㄱ멀리서보는게 ㄱ그글그렇 ㄱ하자ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ그렇게 ㄴ자형으로 ㄷㄷ ㄷㄷㄷ ㄷㄷㄷㄷ ㄷㄷ나처럼 ㄷㄷ다른반 ㄷㅐ게빵도 ㄸ물이라 ㄸ물이라고 ㄹㅇ ㅁ나들어진 ㅁ몇번 ㅂㅏ다 ㅅㄴ천문학관도 ㅅㅂㄴ은 ㅇㅁㅇ ㅇㅅㅇ ㅇㅇ ㅇㅇ다음편은 ㅇㅇ올레길처럼 ㅇㅇ지출 ... 힘쓰셨는데 힘아리없는 힘없던 힘에 힘으로 힘은 힘을 힘을내어보쟈 힘이 힘이겠지 힘이들었는지한입 힘이번주 힘입니다 힘입어 힘줬습니다 힘차게 힘찬 힙시트는 힙한 힙한곳이 힙합 힛약 힝내면속에 힝홍 힝힝
양양 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.044129 0.0 0.0 0.0 0.0 0.0
군산 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
평창 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
태안 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
함양 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.019218 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
포항 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.032732 0.0 0.028297 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
속초 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
하동 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
제주 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
하동 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0

734 rows × 114450 columns

7.1 아이템 기반 여행지 추천

  • 위에서 만들어진 여행지역이름 * 벡터화된 던어의 코사인 유사도 행렬을 통해 추천시스템 함수를 구현하겠습니다.
  • 아이템 기반 여행지 추천시스템은 품사 관계없이 학습된 데이터 내에 포함된 범위 내에서 특정 단어를 입력하면 유사도가 높은 특정 지역을 추천해주는 구조을 가지고 있습니다.
  • 예를 들어, '바다'를 검색하면, '바다'와 유사도가 높은 상위 5개의 지역을 추천해줍니다.
In [96]:
def item_to_area_recommend(keyword):
    sorted_result = df_tfidv[keyword].sort_values(ascending = False).head()
    area_list = []
    dict_result = {}
    for area, sim in sorted_result.items():
        area_list.append(area)
        if area_list.count(area) == 1:
            dict_result[area] = sim
        else:
            dict_result[area] = sorted_result.loc[area].mean()
    series_result = pd.Series(dict_result)

    return series_result
In [97]:
item_to_area_recommend('바다')
Out[97]:
남해    0.142776
강릉    0.114716
여수    0.091131
속초    0.088888
부산    0.087132
dtype: float64
  • 위에서 구현한 item_to_area_recommend 함수에 '바다' 라는 키워드를 입력하면 5개의 지역이 출력됩니다.

7.2 유사 여행지 추천

  • 본 추천시스템은 특정 여행지를 입력하면 유사도가 높은 여행지를 추천해주는 시스템입니다.
  • 예를 들어, '대구' 라고 입력하면, 유사도가 높은 특정 여행지를 추천해줍니다.
In [98]:
from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer(stop_words = stop_words)
tfidf_matrix = tfidf.fit_transform(area_notnull['reviesed_contents'])

print(tfidf_matrix.shape)
(734, 114450)
In [99]:
from sklearn.metrics.pairwise import linear_kernel
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
In [100]:
indices = pd.Series(area_notnull.index, index=area_notnull['area']).drop_duplicates()
print(indices.head())
area
양양    0
군산    1
평창    2
태안    3
함양    4
dtype: int64
In [101]:
def area_to_area_recommend(area, cosine_sim=cosine_sim):
    
    # 지역이름으로부터 해당되는 인덱스를 받아옵니다.
    idx = indices[area]

    # 모든 지역에 대해서 해당 지역과의 유사도를 구합니다.
    sim_scores = list(enumerate(cosine_sim[idx]))

    # 유사도에 따라 지역들을 정렬합니다.
    sim_scores = sorted(sim_scores, key=lambda x: x[0], reverse=True)

    # 가장 유사한 10개의 지역을 받아옵니다.
    sim_scores = sim_scores[1:11]

    # 가장 유사한 10개의 지역의 인덱스를 받아옵니다.
    travel_indices = [i[0] for i in sim_scores]

    # 가장 유사한 10개의 지역이름을 리턴합니다.
    similar_area = [] 
    for area in area_notnull['area'].loc[travel_indices].values:
        if area not in similar_area:
            similar_area.append(area)
    
    return pd.Series(similar_area)
In [102]:
area_to_area_recommend('대구')
Out[102]:
0    제주
1    군산
2    전주
3    동해
4    속초
5    부여
6    평창
7    여수
8    단양
dtype: object
  • 위에서 만든 area_to_area_recommend 함수에 '대구' 지역이름을 입력하면 10개의 지역을 추천해줍니다.

8. 종합결과

8.1 TF-IDF

In [103]:
tfidf_dict_df.sort_values(by = 'TF-IDF', ascending = False).head(20)
Out[103]:
TF-IDF
혼자 179.380080
여행 161.824720
사진 91.948634
국내 70.120795
추천 45.240441
제주도 39.494124
처음 39.132916
나름 37.511477
요즘 37.318147
강원도 34.830206
지금 34.032511
경주 33.352565
시간 33.041031
카페 31.230989
서울 28.555330
부산 27.914304
바다 27.678002
여수 24.781930
마지막 24.091230
오늘 24.082493
  • TF-IDF는 문서 내에서 특정 단어가 갖는 중요도를 나타내는 가중치입니다. 문서 내에서 특정 단어가 출현하는 빈도와 특정 단어를 갖고 있는 문서가 전체 문서에서 차지하는 비율의 역수에 로그를 취한 값을 곱하여 값을 구한다. 이 값이 클수록 중요도가 높다고 간주합니다.
  • TF-IDF 계산 결과, 혼자(179.38)와 여행(161.825)가 가장 중요한 단어라고 나타났습니다. 이는 크롤링 단계에서 검색어에 포함된 단어라는 공통점이 있습니다. 이를 제외한 단어 중에서 살펴보면, 사진(91.949)이 가장 높게 나타났으며, 다음으로 추천(45.24), 제주도(39.494) 등으로 TF-IDF 값이 높았습니다.

8.2 중심성

In [104]:
total_word.head(15)
Out[104]:
위세_단어 위세중심성 연결_단어 연결중심성 근접_단어 근접중심성 매개_단어 매개중심성
0 여행 0.603552 혼자 1.000000 방법 0.563991 국내 0.006408
1 국내 0.553467 여행 1.000000 홈페이지 0.562771 여행 0.006408
2 혼자 0.209561 사진 1.000000 새벽 0.562771 시간 0.006408
3 추천 0.173318 국내 1.000000 영상 0.562771 혼자 0.006408
4 시간 0.135866 시간 1.000000 동네 0.559140 사진 0.006408
5 사진 0.121120 추천 0.996154 태안 0.559140 생각 0.006393
6 생각 0.119798 사람 0.996154 규모 0.559140 사람 0.006370
7 코스 0.105725 생각 0.996154 사장 0.559140 코스 0.006367
8 사람 0.094429 바다 0.988462 플레이스 0.557940 추천 0.006361
9 여자 0.091146 코스 0.988462 중간 0.556745 날씨 0.006304
10 바다 0.076083 날씨 0.984615 구입 0.556745 방문 0.006302
11 시작 0.067760 방문 0.984615 관람 0.555556 바다 0.006297
12 강원도 0.066448 시작 0.980769 수목원 0.555556 유명 0.006260
13 서울 0.065354 유명 0.976923 경험 0.555556 시작 0.006256
14 제주도 0.065329 처음 0.965385 경치 0.554371 거리 0.006204
  • 중심성이란 행위자(node)가 네트워크에서 중심에 위치하는 정도를 계량적으로 보여주는 개념입니다. 값이 높을수록 해당 노드는 네트워크 내에서 중요하다고 간주합니다.
  • 네트워크 분석에서 사용된 위세 중심성(노드와 연결된 다른 노드의 영향력을 통해 해당 노드의 중요성을 계산하는 개념)을 살펴보면, TF-IDF 결과와 유사하게 추천(0.173)과 시간(0.136)이 중요한 단어임을 알 수 있습니다. 이외 사진(0.121), 생각(0.12) 등의 순이었습니다. 하지만, 여행, 국내를 제외하면 값이 작아 전체적으로 영향력이 있다고 해석할 수 없습니다.

8.3 네트워크 그림

In [105]:
plt.figure(figsize=(25, 20))

colors = [new_travel_word2id_cluster6[n] for n in travel_network.nodes()]
my_colors = plt.cm.Set2_r

nx.draw(travel_network, with_labels = True, labels = travel_id2word, font_family = 'Malgun Gothic', 
        node_color=colors, cmap = my_colors, font_size = 14, edge_color = '#D4D5CE', font_weight='bold')
  • 각 클러스터에 해당하는 단어는 아래 함수를 통해 출력하겠습니다.
In [106]:
def cluster_output(data, cluster):
    return data[data['cluster'] == cluster]

print('0번 클러스터')
print(cluster_output(travel_results, 0).head())
print('\n1번 클러스터')
print(cluster_output(travel_results, 1).head())
print('\n2번 클러스터')
print(cluster_output(travel_results, 2).head())
print('\n3번 클러스터')
print(cluster_output(travel_results, 3).head())
print('\n4번 클러스터')
print(cluster_output(travel_results, 4).head())
print('\n5번 클러스터')
print(cluster_output(travel_results, 5).head())
0번 클러스터
           x         y  cluster
국내 -7.803099  5.821302        0
바다 -6.073950  2.216973        0
공원 -4.658981  4.268764        0
날씨 -9.263393  1.686446        0
준비 -5.364696  1.126775        0

1번 클러스터
            x         y  cluster
여행   5.969330  6.038617        1
시간   4.000901 -0.413756        1
카페   7.741916 -2.714547        1
제주도  4.052032  2.697602        1
공간   5.685294  3.675050        1

2번 클러스터
           x         y  cluster
마을 -9.579128 -1.465526        2
풍경 -4.536358 -4.529608        2
시작 -3.908912 -1.573727        2
축제 -8.184956 -1.088954        2
여수 -7.390391 -0.768201        2

3번 클러스터
           x         y  cluster
생각 -2.405668  4.484372        3
사람 -1.920977  9.232206        3
경주 -1.102257  8.455402        3
부산  0.917647  5.439097        3
가을 -3.316177  5.317612        3

4번 클러스터
           x         y  cluster
추천 -0.514411 -6.934916        4
코스  0.975226 -8.499249        4
버스  6.977162 -5.753626        4
구경  0.142482 -5.269048        4
서울  5.749057 -4.819408        4

5번 클러스터
            x         y  cluster
사진  -0.046423 -2.585906        5
혼자  -3.113740 -1.154953        5
호텔  -2.173965 -0.420692        5
제주   0.655873 -1.276237        5
강원도 -1.949614 -3.466742        5

8.4 단어 간 유사도

In [107]:
# 두 단어 간 유사도를 구하겠습니다.

def two_word_sim(x, y):
    sim = model.wv.similarity(x, y)
    sim_series = pd.Series(sim)
    df = pd.DataFrame(sim_series)
    df.columns = [y]
    df.index = [x]
    return df

two_word_sim('대구', '막창')
Out[107]:
막창
대구 0.571788
In [108]:
# 특정 단어와 유사도가 높은 순으로 10개를 출력하겠습니다.

def sim_table(x):
    return pd.DataFrame(model.wv.most_similar(x, topn = 200), columns = ['단어', '유사도'])

df = sim_table('해수욕장')
df.head(10)
Out[108]:
단어 유사도
0 대천 0.871096
1 하조대 0.863719
2 변산 0.848637
3 광안리 0.833906
4 송정 0.820823
5 함덕 0.813584
6 동호 0.808508
7 서해 0.806977
8 죽포 0.794449
9 수영구 0.791806
  • 임의로 입력한 단어인 해수욕장과 유사한 단어 10개를 출력한 결과입니다.
  • 간략하게 결과를 살펴보면, 하조대, 대천, 변산, 광안리 등 해수욕장 이름이 유사 결과로 출력되었습니다.

8.5 여행지 추천시스템

In [109]:
def item_to_area_recommend(keyword):
    sorted_result = df_tfidv[keyword].sort_values(ascending = False).head()
    area_list = []
    dict_result = {}
    for area, sim in sorted_result.items():
        area_list.append(area)
        if area_list.count(area) == 1:
            dict_result[area] = sim
        else:
            dict_result[area] = sorted_result.loc[area].mean()
    series_result = pd.Series(dict_result)

    return series_result
In [110]:
item_to_area_recommend('바다')
Out[110]:
남해    0.142776
강릉    0.114716
여수    0.091131
속초    0.088888
부산    0.087132
dtype: float64
  • 임의로 입력한 단어와 유사도가 높은 지역 5개를 추천되었습니다.
In [ ]: